Compare commits

..
Author SHA1 Message Date
Felix Rieseberg ebf421ebed wip: Properly select Node version 2019-12-12 15:20:13 -05:00
Felix Rieseberg 41409f578e wip: Even more stuff 2019-12-12 14:58:34 -05:00
285 changed files with 13201 additions and 18834 deletions
-4
View File
@@ -1,4 +0,0 @@
/out
/dist
/coverage
/static
-30
View File
@@ -1,30 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
extends: [
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
},
};
-31
View File
@@ -1,31 +0,0 @@
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
@@ -1,51 +0,0 @@
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,9 +31,6 @@ yarn-error.log
# Coverage
coverage
# Test report
report.json
# npm package
/npm/dist
/npm/path.txt
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
trailingComma: "all",
singleQuote: true,
};
+4 -3
View File
@@ -3,12 +3,13 @@ node_js: "12"
os:
- linux
- osx
dist: bionic
dist: trusty
osx_image: xcode10
sudo: false
cache:
npm: true
directories:
- node_modules
- $HOME/.cache/electron
addons:
@@ -23,7 +24,7 @@ branches:
- /^v\d+\.\d+\.\d+/
install:
- npm ci
- npm install
- |
if [[ "$TRAVIS_OS_NAME" == "osx" && "$TRAVIS_SECURE_ENV_VARS" == "true" ]]; then
export CERTIFICATE_P12=cert.p12;
-74
View File
@@ -1,74 +0,0 @@
# 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
1. Clone the Electron Fiddle repository locally.
```sh
git clone https://github.com/electron/fiddle.git
```
2. Change directory to where Fiddle has been cloned.
```sh
cd fiddle
```
3. Install dependencies.
```sh
npm install
```
4. Start Fiddle and Explore!
```sh
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-2020 GitHub Inc.
Copyright (c) 2013-2018 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.
+1 -7
View File
@@ -1,8 +1,6 @@
# <img src="https://user-images.githubusercontent.com/378023/49785546-4b7f7000-fd64-11e8-8033-a52c73a07fbf.png" width="60px" align="center" alt="Electron Fiddle icon"> Electron Fiddle
[![Build Status](https://travis-ci.org/electron/fiddle.svg?branch=master)](https://travis-ci.org/electron/fiddle)
[![Coverage Status](https://coveralls.io/repos/github/electron/fiddle/badge.svg?branch=master)](https://coveralls.io/github/electron/fiddle?branch=master)
[![Electron Discord Invite](https://img.shields.io/discord/745037351163527189?color=%237289DA&label=chat&logo=discord&logoColor=white)](https://discord.com/invite/electron)
[![Build Status](https://travis-ci.org/electron/fiddle.svg?branch=master)](https://travis-ci.org/electron/fiddle) [![Coverage Status](https://coveralls.io/repos/github/electron/fiddle/badge.svg?branch=master)](https://coveralls.io/github/electron/fiddle?branch=master)
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,
@@ -58,10 +56,6 @@ fiddle has grown up, export it as a project with or without
[electron-forge][electron-forge]. Then, use your favorite editor and take on
the world!
## Contributing
Electron Fiddle is a community-driven project that welcomes all sorts of contributions. Please check out our [Contributing Guide](https://github.com/electron/fiddle/blob/master/CONTRIBUTING.md) for more details.
## License
[MIT, please see the LICENSE file for full details](https://github.com/electron/fiddle/blob/master/LICENSE.md).
+42 -58
View File
@@ -1,33 +1,32 @@
const path = require('path');
const fs = require('fs');
const packageJson = require('./package.json');
/* tslint:disable */
const { version } = packageJson;
const iconDir = path.resolve(__dirname, 'assets', 'icons');
const path = require('path')
const fs = require('fs')
const packageJson = require('./package.json')
const { version } = packageJson
const iconDir = path.resolve(__dirname, 'assets', 'icons')
const config = {
hooks: {
generateAssets: require('./tools/generateAssets'),
generateAssets: require('./tools/generateAssets')
},
packagerConfig: {
name: 'Electron Fiddle',
executableName: 'electron-fiddle',
asar: true,
icon: path.resolve(__dirname, 'assets', 'icons', 'fiddle'),
// TODO: FIXME?
// ignore: [
// /^\/\.vscode\//,
// /^\/tools\//
// ],
appBundleId: 'com.electron.fiddle',
usageDescription: {
Camera:
'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Camera',
Microphone:
'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Microphone',
},
appCategoryType: 'public.app-category.developer-tools',
protocols: [
{
name: 'Electron Fiddle Launch Protocol',
schemes: ['electron-fiddle'],
},
],
protocols: [{
name: 'Electron Fiddle Launch Protocol',
schemes: ['electron-fiddle']
}],
win32metadata: {
CompanyName: 'Electron Community',
OriginalFilename: 'Electron Fiddle',
@@ -36,10 +35,10 @@ const config = {
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
'hardened-runtime': true,
'gatekeeper-assess': false,
entitlements: 'static/entitlements.plist',
'entitlements': 'static/entitlements.plist',
'entitlements-inherit': 'static/entitlements.plist',
'signature-flags': 'library',
},
'signature-flags': 'library'
}
},
makers: [
{
@@ -51,44 +50,41 @@ const config = {
: process.env.WINDOWS_CERTIFICATE_FILE;
if (!certificateFile || !fs.existsSync(certificateFile)) {
console.warn(
`Warning: Could not find certificate file at ${certificateFile}`,
);
console.warn(`Warning: Could not find certificate file at ${certificateFile}`)
}
return {
name: 'electron-fiddle',
authors: 'Electron Community',
exe: 'electron-fiddle.exe',
iconUrl:
'https://raw.githubusercontent.com/electron/fiddle/0119f0ce697f5ff7dec4fe51f17620c78cfd488b/assets/icons/fiddle.ico',
iconUrl: 'https://raw.githubusercontent.com/electron/fiddle/0119f0ce697f5ff7dec4fe51f17620c78cfd488b/assets/icons/fiddle.ico',
loadingGif: './assets/loading.gif',
noMsi: true,
remoteReleases: '',
setupExe: `electron-fiddle-${version}-${arch}-setup.exe`,
setupIcon: path.resolve(iconDir, 'fiddle.ico'),
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD,
certificateFile,
};
},
certificateFile
}
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
platforms: ['darwin']
},
{
name: '@electron-forge/maker-deb',
platforms: ['linux'],
config: {
icon: {
scalable: path.resolve(iconDir, 'fiddle.svg'),
},
},
scalable: path.resolve(iconDir, 'fiddle.svg')
}
}
},
{
name: '@electron-forge/maker-rpm',
platforms: ['linux'],
},
platforms: ['linux']
}
],
publishers: [
{
@@ -96,39 +92,27 @@ const config = {
config: {
repository: {
owner: 'electron',
name: 'fiddle',
name: 'fiddle'
},
draft: true,
prerelease: false,
},
},
],
};
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!',
);
console.warn('Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!');
return;
}
@@ -136,11 +120,11 @@ function notarizeMaybe() {
appBundleId: 'com.electron.fiddle',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
ascProvider: 'LT94ZKYDCJ',
};
ascProvider: 'LT94ZKYDCJ'
}
}
notarizeMaybe();
notarizeMaybe()
// Finally, export it
module.exports = config;
module.exports = config
+2 -5
View File
@@ -1,15 +1,12 @@
{
"transform": {
"\\.(js|jsx|ts|tsx)$": "ts-jest"
".(js|jsx|ts|tsx)": "ts-jest"
},
"testURL": "http://localhost",
"testRegex": "(-spec)\\.(ts|tsx)$",
"resetMocks": true,
"bail": true,
"resetMocks": true,
"resetModules": true,
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"setupFilesAfterEnv": [
"<rootDir>/tests/setup.js"
],
+7893 -9744
View File
File diff suppressed because it is too large Load Diff
+60 -82
View File
@@ -1,29 +1,29 @@
{
"name": "electron-fiddle",
"productName": "Electron Fiddle",
"version": "0.16.0",
"version": "0.11.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:ts": "eslint \"./**/*.{js,ts,tsx}\" --fix",
"lint:templates": "standard \"./static/show-me/**/*.js\" --fix",
"lint": "npm-run-all \"lint:*\"",
"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:*",
"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",
"test": "jest --config=jest.json --coverage",
"test:ci": "jest --config=jest.json --coverage --runInBand",
"test:coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.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"
"tsc": "node ./tools/tsc.js",
"electron-releases": "node ./tools/fetch-releases.js"
},
"keywords": [
"Electron",
@@ -36,95 +36,73 @@
"forge": "./forge.config.js"
},
"dependencies": {
"@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",
"@blueprintjs/core": "^3.19.1",
"@blueprintjs/select": "^3.11.1",
"@octokit/rest": "^16.34.0",
"builtin-modules": "^3.1.0",
"classnames": "^2.2.6",
"electron-default-menu": "^1.0.2",
"electron-devtools-installer": "^3.1.1",
"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": "^3.0.0",
"fix-path": "^2.1.0",
"fs-extra": "^8.1.0",
"mobx": "^5.15.4",
"mobx-react": "^6.1.8",
"monaco-editor": "^0.20.0",
"mobx": "^5.14.2",
"mobx-react": "^6.1.4",
"monaco-editor": "^0.18.1",
"monaco-loader": "1.0.0",
"namor": "^2.0.2",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"namor": "^1.1.3",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-mosaic-component": "^3.2.0",
"semver": "^7.1.3",
"semver": "^6.3.0",
"tmp": "0.1.0",
"tslib": "^1.11.1",
"update-electron-app": "^2.0.1"
"tslib": "^1.10.0",
"update-electron-app": "^1.5.0"
},
"devDependencies": {
"@babel/core": "^7.8.6",
"@electron-forge/cli": "^6.0.0-beta.52",
"@electron-forge/maker-deb": "^6.0.0-beta.52",
"@electron-forge/maker-rpm": "^6.0.0-beta.52",
"@electron-forge/maker-squirrel": "^6.0.0-beta.52",
"@electron-forge/maker-zip": "^6.0.0-beta.52",
"@electron-forge/publisher-github": "^6.0.0-beta.52",
"@octokit/action": "^2.0.0",
"@babel/core": "^7.6.4",
"@electron-forge/cli": "^6.0.0-beta.46",
"@electron-forge/maker-deb": "^6.0.0-beta.46",
"@electron-forge/maker-rpm": "^6.0.0-beta.46",
"@electron-forge/maker-squirrel": "^6.0.0-beta.46",
"@electron-forge/maker-zip": "^6.0.0-beta.46",
"@electron-forge/publisher-github": "^6.0.0-beta.46",
"@types/builtin-modules": "^3.1.1",
"@types/classnames": "^2.2.9",
"@types/enzyme": "^3.10.5",
"@types/fs-extra": "^8.1.0",
"@types/jest": "^25.1.3",
"@types/enzyme": "^3.10.3",
"@types/fs-extra": "^8.0.1",
"@types/jest": "^24.0.19",
"@types/log-symbols": "^3.0.0",
"@types/node": "^13.7.6",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"@types/semver": "^7.1.0",
"@types/node": "^12.11.7",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.3",
"@types/semver": "^6.0.2",
"@types/tmp": "0.1.0",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"chokidar": "^3.3.1",
"coveralls": "^3.0.9",
"electron": "^9.1.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.3",
"fetch-mock-jest": "^1.1.0-beta.3",
"husky": "^4.2.5",
"jest": "^25.1.0",
"less": "^3.11.1",
"lint-staged": "^10.2.11",
"chokidar": "^3.2.2",
"coveralls": "^3.0.7",
"electron": "7.1.3",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"enzyme-to-json": "^3.4.3",
"jest": "^24.9.0",
"jest-fetch-mock": "^2.1.2",
"less": "^3.10.3",
"log-symbols": "^3.0.0",
"node-abi": "^2.18.0",
"node-fetch": "^2.6.1",
"node-abi": "^2.12.0",
"node-fetch": "^2.6.0",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.4",
"prettier": "^2.0.5",
"react-test-renderer": "^16.13.0",
"react-test-renderer": "^16.11.0",
"rimraf": "^3.0.0",
"standard": "^14.3.1",
"stylelint": "^13.2.0",
"stylelint-config-standard": "^20.0.0",
"ts-jest": "^25.2.1",
"typescript": "^3.8.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"./**/*.{js,ts,tsx}": [
"eslint --fix"
],
"./static/show-me/**/*.js": [
"standard --fix"
],
"./src/less/*.less": [
"stylelint --fix"
]
"stylelint": "^11.1.1",
"stylelint-config-standard": "^19.0.0",
"ts-jest": "^24.1.0",
"tslint": "^5.20.0",
"tslint-microsoft-contrib": "^6.2.0",
"tslint-react": "^4.1.0",
"typescript": "^3.6.4"
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ declare global {
interface Window {
ElectronFiddle: {
app: AppType;
contentChangeListeners: Array<any>;
contentChangeListeners: Array<any>,
editors: Record<EditorId, MonacoType.editor.IStandaloneCodeEditor | null>;
};
}
+1 -1
View File
@@ -3,7 +3,6 @@ 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>
@@ -18,3 +17,4 @@ export const html = `<!DOCTYPE html>
</script>
</body>
</html>`;
+14 -2
View File
@@ -3,9 +3,13 @@ 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.
const mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
mainWindow.loadURL(url.format({
@@ -16,6 +20,14 @@ 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
@@ -35,7 +47,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 (BrowserWindow.getAllWindows().length === 0) {
if (mainWindow === null) {
createWindow()
}
})
+18 -6
View File
@@ -1,9 +1,13 @@
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.
const mainWindow = new BrowserWindow({
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
@@ -16,17 +20,25 @@ 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
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(createWindow)
app.on('ready', createWindow)
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
@@ -35,7 +47,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 (BrowserWindow.getAllWindows().length === 0) {
if (mainWindow === null) {
createWindow()
}
})
+20 -43
View File
@@ -7,47 +7,38 @@ export type FileTransform = (files: Files) => Promise<Files>;
export enum VersionState {
ready = 'ready',
downloading = 'downloading',
unzipping = 'unzipping',
unknown = 'unknown',
unknown = 'unknown'
}
export enum VersionSource {
export enum ElectronVersionSource {
remote = 'remote',
local = 'local',
}
export enum GistActionType {
publish = 'Publish',
update = 'Update',
delete = 'Delete',
}
export enum GistActionState {
publishing = 'publishing',
updating = 'updating',
deleting = 'deleting',
none = 'none',
local = 'local'
}
export interface Version {
version: string;
}
export interface NpmVersion extends Version {
name?: string;
localPath?: string;
}
export interface NodeVersion extends Version {
state: VersionState;
}
export interface EditorValues {
main: string;
renderer: string;
html: string;
preload: string;
css: string;
package?: string;
}
export interface RunnableVersion extends Version {
export interface ElectronVersion extends NpmVersion {
state: VersionState;
source: VersionSource;
downloadProgress?: number;
source: ElectronVersionSource;
}
export interface SetFiddleOptions {
@@ -67,52 +58,38 @@ export interface OutputOptions {
isNotPre?: boolean;
}
export interface GenericDialogOptions {
type: GenericDialogType;
export interface WarningDialogTexts {
ok?: string;
cancel?: string;
label: string | JSX.Element;
label: string;
}
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',
'preload' = 'preload',
'css' = 'css',
'preload' = 'preload'
}
// Panels that can show up as a mosaic
export const enum PanelId {
'docsDemo' = 'docsDemo',
'docsDemo' = 'docsDemo'
}
export type MosaicId = EditorId | PanelId;
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 const ALL_EDITORS = [ EditorId.main, EditorId.renderer, EditorId.preload, EditorId.html ];
export const ALL_PANELS = [ PanelId.docsDemo ];
export const ALL_MOSAICS = [ ...ALL_EDITORS, ...ALL_PANELS ];
export type ArrowPosition = 'top' | 'left' | 'bottom' | 'right';
export const enum DocsDemoPage {
DEFAULT = 'DEFAULT',
DEMO_APP = 'DEMO_APP',
DEMO_APP = 'DEMO_APP'
}
+3 -14
View File
@@ -23,11 +23,6 @@ export enum IpcEvents {
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',
BEFORE_QUIT = 'BEFORE_QUIT',
CONFIRM_QUIT = 'CONFIRM_QUIT',
SET_APPDATA_DIR = 'SET_APPDATA_DIR',
SELECT_ALL_IN_EDITOR = 'SELECT_ALL_IN_EDITOR',
}
export const ipcMainEvents = [
@@ -35,9 +30,7 @@ export const ipcMainEvents = [
IpcEvents.FS_SAVE_FIDDLE,
IpcEvents.SHOW_WARNING_DIALOG,
IpcEvents.SHOW_CONFIRMATION_DIALOG,
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG,
IpcEvents.SHOW_INACTIVE,
IpcEvents.CONFIRM_QUIT,
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG
];
export const ipcRendererEvents = [
@@ -59,11 +52,7 @@ export const ipcRendererEvents = [
IpcEvents.SHOW_WELCOME_TOUR,
IpcEvents.CLEAR_CONSOLE,
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
IpcEvents.BISECT_COMMANDS_TOGGLE,
IpcEvents.BEFORE_QUIT,
IpcEvents.SET_APPDATA_DIR,
IpcEvents.SELECT_ALL_IN_EDITOR,
IpcEvents.BISECT_COMMANDS_TOGGLE
];
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL =
'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = 'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
-15
View File
@@ -24,21 +24,6 @@
}
}
// 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; }
-5
View File
@@ -1,5 +0,0 @@
.bp3-fill {
.version-chooser {
width: 100%;
}
}
-1
View File
@@ -17,4 +17,3 @@
@import "components/editors.less";
@import "components/tour.less";
@import "components/show-me.less";
@import "components/version-select.less";
+42 -54
View File
@@ -1,9 +1,4 @@
import {
BrowserWindow,
ContextMenuParams,
Menu,
MenuItemConstructorOptions,
} from 'electron';
import { BrowserWindow, ContextMenuParams, Menu, MenuItemConstructorOptions } from 'electron';
import { IpcEvents } from '../ipc-events';
import { isDevMode } from '../utils/devmode';
import { ipcMainManager } from './ipc';
@@ -18,16 +13,16 @@ export function getRunItems(): Array<MenuItemConstructorOptions> {
{
id: 'run',
label: 'Run Fiddle',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN),
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
},
{
id: 'clear_console',
label: 'Clear Console',
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE),
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE)
},
{
type: 'separator',
},
type: 'separator'
}
];
}
@@ -40,10 +35,9 @@ export function getRunItems(): Array<MenuItemConstructorOptions> {
* @param {ContextMenuParams} { x, y }
* @returns {Array<MenuItemConstructorOptions>}
*/
export function getMonacoItems({
pageURL,
editFlags,
}: ContextMenuParams): Array<MenuItemConstructorOptions> {
export function getMonacoItems(
{ pageURL, editFlags }: ContextMenuParams
): Array<MenuItemConstructorOptions> {
if (!editFlags.canPaste || !/.*index\.html(#?)$/.test(pageURL || '')) {
return [];
}
@@ -53,51 +47,51 @@ export function getMonacoItems({
id: 'go_to_definition',
label: 'Go to Definition',
click() {
const cmd = ['editor.action.goToDeclaration'];
const cmd = [ 'editor.action.goToDeclaration' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{
id: 'peek_definition',
label: 'Peek Definition',
click() {
const cmd = ['editor.action.previewDeclaration'];
const cmd = [ 'editor.action.previewDeclaration' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{
id: 'references',
label: 'Find References',
click() {
const cmd = ['editor.action.referenceSearch.trigger'];
const cmd = [ 'editor.action.referenceSearch.trigger' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{ type: 'separator' },
{
id: 'palette',
label: 'Command Palette',
click() {
const cmd = ['editor.action.quickCommand'];
const cmd = [ 'editor.action.quickCommand' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{ type: 'separator' },
{
id: 'format_document',
label: 'Format Document',
click() {
const cmd = ['editor.action.formatDocument'];
const cmd = [ 'editor.action.formatDocument' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{
id: 'format_selection',
label: 'Format Selection',
click() {
const cmd = ['editor.action.formatSelection'];
const cmd = [ 'editor.action.formatSelection' ];
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
},
}
},
{ type: 'separator' },
];
@@ -111,28 +105,25 @@ export function getMonacoItems({
* @returns {Array<MenuItemConstructorOptions>}
*/
export function getInspectItems(
browserWindow: BrowserWindow,
{ x, y }: ContextMenuParams,
browserWindow: BrowserWindow, { x, y }: ContextMenuParams
): Array<MenuItemConstructorOptions> {
if (!isDevMode()) return [];
return [
{
id: 'inspect',
label: 'Inspect Element',
click: () => {
browserWindow.webContents.inspectElement(x, y);
return [{
id: 'inspect',
label: 'Inspect Element',
click: () => {
browserWindow.webContents.inspectElement(x, y);
try {
if (browserWindow.webContents.isDevToolsOpened()) {
browserWindow.webContents.devToolsWebContents?.focus();
}
} catch (error) {
console.warn(`Tried to focus dev tools, but failed`, { error });
try {
if (browserWindow.webContents.isDevToolsOpened()) {
browserWindow.webContents.devToolsWebContents.focus();
}
},
},
];
} catch (error) {
console.warn(`Tried to focus dev tools, but failed`, { error });
}
}
}];
}
/**
@@ -151,24 +142,21 @@ export function createContextMenu(browserWindow: BrowserWindow) {
id: 'cut',
label: 'Cut',
role: 'cut',
enabled: editFlags.canCut,
},
{
enabled: editFlags.canCut
}, {
id: 'copy',
label: 'Copy',
role: 'copy',
enabled: editFlags.canCopy,
},
{
enabled: editFlags.canCopy
}, {
id: 'paste',
label: 'Paste',
role: 'paste',
enabled: editFlags.canPaste,
enabled: editFlags.canPaste
}, {
type: 'separator'
},
{
type: 'separator',
},
...getInspectItems(browserWindow, props),
...getInspectItems(browserWindow, props)
];
const menu = Menu.buildFromTemplate(template);
+1 -1
View File
@@ -12,7 +12,7 @@ export async function setupDevTools(): Promise<void> {
const {
default: installExtension,
REACT_DEVELOPER_TOOLS,
REACT_PERF,
REACT_PERF
} = require('electron-devtools-installer');
try {
+7 -9
View File
@@ -17,12 +17,10 @@ export function setupDialogs() {
showConfirmationDialog(args);
});
ipcMainManager.on(
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG,
async (event) => {
await showOpenDialog(event);
},
);
ipcMainManager.on(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, async (event) => {
await showOpenDialog(event);
});
}
/**
@@ -33,7 +31,7 @@ export function setupDialogs() {
function showWarningDialog(args: Electron.MessageBoxOptions) {
dialog.showMessageBox(getOrCreateMainWindow(), {
type: 'warning',
...args,
...args
});
}
@@ -45,14 +43,14 @@ function showWarningDialog(args: Electron.MessageBoxOptions) {
function showConfirmationDialog(args: Electron.MessageBoxOptions) {
dialog.showMessageBox(getOrCreateMainWindow(), {
type: 'warning',
...args,
...args
});
}
async function showOpenDialog(event: IpcMainEvent) {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Folder',
properties: ['openDirectory'],
properties: ['openDirectory']
});
if (!filePaths || filePaths.length < 1) {
+7 -12
View File
@@ -3,12 +3,7 @@ import * as fs from 'fs-extra';
import * as path from 'path';
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, RENDERER_JS_NAME } from '../shared-constants';
import { ipcMainManager } from './ipc';
/**
@@ -27,14 +22,14 @@ export function setupFileListeners() {
export async function showOpenDialog() {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Fiddle',
properties: ['openDirectory'],
properties: ['openDirectory']
});
if (!filePaths || filePaths.length < 1) {
return;
}
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [filePaths[0]]);
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
}
/**
@@ -46,7 +41,7 @@ export async function showSaveDialog(event?: IpcEvents, as?: string) {
const { filePaths } = await dialog.showOpenDialog({
buttonLabel: 'Save here',
properties: ['openDirectory', 'createDirectory'],
title: `Save Fiddle${as ? ` as ${as}` : ''}`,
title: `Save Fiddle${as ? ` as ${as}` : ''}`
});
if (!filePaths || filePaths.length < 1) {
@@ -57,7 +52,7 @@ export async function showSaveDialog(event?: IpcEvents, as?: string) {
// Let's confirm real quick if we want this
if (await ensureSaveTargetEmpty(filePaths[0])) {
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [filePaths[0]]);
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
}
}
@@ -72,7 +67,7 @@ async function ensureSaveTargetEmpty(filePath: string): Promise<boolean> {
path.join(filePath, INDEX_HTML_NAME),
path.join(filePath, RENDERER_JS_NAME),
path.join(filePath, MAIN_JS_NAME),
path.join(filePath, PACKAGE_NAME),
path.join(filePath, PACKAGE_NAME)
];
let noFilesOrOverwriteGranted = true;
@@ -97,7 +92,7 @@ async function confirmFileOverwrite(filePath: string): Promise<boolean> {
try {
const result = await dialog.showMessageBox({
type: 'warning',
buttons: ['Cancel', 'Yes'],
buttons: [ 'Cancel', 'Yes' ],
message: 'Overwrite files?',
detail: `The file ${filePath} already exists. Do you want to overwrite it?`,
});
+12 -27
View File
@@ -1,11 +1,7 @@
import { ipcMain } from 'electron';
import { EventEmitter } from 'events';
import {
IpcEvents,
ipcMainEvents,
WEBCONTENTS_READY_FOR_IPC_SIGNAL,
} from '../ipc-events';
import { IpcEvents, ipcMainEvents, WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { getOrCreateMainWindow } from './windows';
/**
@@ -18,32 +14,25 @@ import { getOrCreateMainWindow } from './windows';
*/
export class IpcMainManager extends EventEmitter {
public readyWebContents = new WeakSet<Electron.WebContents>();
private messageQueue = new WeakMap<
Electron.WebContents,
Array<[IpcEvents, Array<any> | undefined]>
>();
private messageQueue = new WeakMap<Electron.WebContents, Array<[IpcEvents, Array<any> | undefined]>>();
constructor() {
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.IpcMainEvent) => {
this.readyWebContents.add(event.sender);
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;
for (const item of queue) {
this.send(item[0], item[1], event.sender);
}
},
);
const queue = this.messageQueue.get(event.sender);
this.messageQueue.delete(event.sender);
if (!queue) return;
for (const item of queue) {
this.send(item[0], item[1], event.sender);
}
});
}
/**
@@ -54,11 +43,7 @@ export class IpcMainManager extends EventEmitter {
* @param {Array<any>} args
* @param {Electron.WebContents} [target]
*/
public send(
channel: IpcEvents,
args?: Array<any>,
target?: Electron.WebContents,
) {
public send(channel: IpcEvents, args?: Array<any>, target?: Electron.WebContents) {
const _target = target || getOrCreateMainWindow().webContents;
const _args = args || [];
if (!this.readyWebContents.has(_target)) {
+3 -18
View File
@@ -1,15 +1,10 @@
import { initSentry } from '../sentry';
initSentry();
import { app } from 'electron';
import { IpcEvents } from '../ipc-events';
import { isDevMode } from '../utils/devmode';
import { setupAboutPanel } from './about-panel';
import { setupAboutPanel } from '../utils/set-about-panel';
import { setupDevTools } from './devtools';
import { setupDialogs } from './dialogs';
import { onFirstRunMaybe } from './first-run';
import { ipcMainManager } from './ipc';
import { listenForProtocolHandler, setupProtocolHandler } from './protocol';
import { shouldQuit } from './squirrel';
import { setupUpdates } from './update';
@@ -43,17 +38,7 @@ export async function onReady() {
* @export
*/
export function onBeforeQuit() {
ipcMainManager.send(IpcEvents.BEFORE_QUIT);
ipcMainManager.on(IpcEvents.CONFIRM_QUIT, quitAppIfConfirmed);
}
export function quitAppIfConfirmed(
_: Electron.IpcMainEvent,
quitConfirmed: boolean,
) {
if (quitConfirmed) {
app.quit();
}
(global as any).isQuitting = true;
}
/**
@@ -89,7 +74,7 @@ export function main() {
listenForProtocolHandler();
// Launch
app.whenReady().then(onReady);
app.on('ready', onReady);
app.on('before-quit', onBeforeQuit);
app.on('window-all-closed', onWindowsAllClosed);
app.on('activate', getOrCreateMainWindow);
+83 -136
View File
@@ -1,10 +1,4 @@
import {
app,
BrowserWindow,
Menu,
MenuItemConstructorOptions,
shell,
} from 'electron';
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
import { Templates } from '../interfaces';
import { IpcEvents } from '../ipc-events';
@@ -20,7 +14,7 @@ import { createMainWindow } from './windows';
* @returns {submenu is Array<Electron.MenuItemConstructorOptions>}
*/
function isSubmenu(
submenu?: Array<MenuItemConstructorOptions> | Menu,
submenu?: Array<MenuItemConstructorOptions> | Menu
): submenu is Array<MenuItemConstructorOptions> {
return !!submenu && Array.isArray(submenu);
}
@@ -33,16 +27,16 @@ function isSubmenu(
function getHelpItems(): Array<MenuItemConstructorOptions> {
return [
{
type: 'separator',
type: 'separator'
},
{
label: 'Show Welcome Tour',
click() {
ipcMainManager.send(IpcEvents.SHOW_WELCOME_TOUR);
},
}
},
{
type: 'separator',
type: 'separator'
},
{
label: 'Toggle Developer Tools',
@@ -51,30 +45,30 @@ function getHelpItems(): Array<MenuItemConstructorOptions> {
const browserWindow = BrowserWindow.getFocusedWindow();
if (browserWindow && !browserWindow.isDestroyed()) {
browserWindow.webContents.toggleDevTools();
browserWindow.webContents.openDevTools({ mode: 'bottom' });
}
},
}
},
{
type: 'separator',
type: 'separator'
},
{
label: 'Open Fiddle Repository...',
click() {
shell.openExternal('https://github.com/electron/fiddle');
},
}
},
{
label: 'Open Electron Repository...',
click() {
shell.openExternal('https://github.com/electron/electron');
},
}
},
{
label: 'Open Electron Issue Tracker...',
click() {
shell.openExternal('https://github.com/electron/electron/issues');
},
}
},
];
}
@@ -88,18 +82,16 @@ function getHelpItems(): Array<MenuItemConstructorOptions> {
function getPreferencesItems(): Array<MenuItemConstructorOptions> {
return [
{
type: 'separator',
},
{
type: 'separator'
}, {
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click() {
ipcMainManager.send(IpcEvents.OPEN_SETTINGS);
},
},
{
type: 'separator',
},
}
}, {
type: 'separator'
}
];
}
@@ -111,11 +103,12 @@ function getPreferencesItems(): Array<MenuItemConstructorOptions> {
function getQuitItems(): Array<MenuItemConstructorOptions> {
return [
{
type: 'separator',
},
{
role: 'quit',
},
type: 'separator'
}, {
label: 'Exit',
accelerator: 'Ctrl+Q,',
click: app.quit
}
];
}
@@ -129,32 +122,29 @@ function getTasksMenu(): MenuItemConstructorOptions {
{
label: 'Run Fiddle...',
accelerator: 'F5',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN),
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
},
{
label: 'Package Fiddle...',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_PACKAGE),
click: () => ipcMainManager.send(IpcEvents.FIDDLE_PACKAGE)
},
{
label: 'Make installers for Fiddle...',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_MAKE),
},
click: () => ipcMainManager.send(IpcEvents.FIDDLE_MAKE)
}
];
return {
label: 'Tasks',
submenu: tasksMenu,
submenu: tasksMenu
};
}
function getShowMeMenuItem(
key: string,
item: string | Templates,
): MenuItemConstructorOptions {
function getShowMeMenuItem(key: string, item: string | Templates): MenuItemConstructorOptions {
if (typeof item === 'string') {
return {
label: key,
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key]),
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key])
};
}
@@ -162,18 +152,17 @@ function getShowMeMenuItem(
label: key,
submenu: Object.keys(item).map((subkey) => {
return getShowMeMenuItem(subkey, item[subkey]);
}),
})
};
}
function getShowMeMenu(): MenuItemConstructorOptions {
const showMeMenu: Array<MenuItemConstructorOptions> = Object.keys(
SHOW_ME_TEMPLATES,
).map((key) => getShowMeMenuItem(key, SHOW_ME_TEMPLATES[key]));
const showMeMenu: Array<MenuItemConstructorOptions> = Object.keys(SHOW_ME_TEMPLATES)
.map((key) => getShowMeMenuItem(key, SHOW_ME_TEMPLATES[key]));
return {
label: 'Show Me',
submenu: showMeMenu,
submenu: showMeMenu
};
}
@@ -187,36 +176,34 @@ function getFileMenu(): MenuItemConstructorOptions {
{
label: 'New Fiddle',
click: () => ipcMainManager.send(IpcEvents.FS_NEW_FIDDLE),
accelerator: 'CmdOrCtrl+N',
},
{
accelerator: 'CmdOrCtrl+N'
}, {
label: 'New Window',
click: () => createMainWindow(),
accelerator: 'CmdOrCtrl+Shift+N',
},
{
type: 'separator',
accelerator: 'CmdOrCtrl+Shift+N'
}, {
type: 'separator'
},
{
label: 'Open',
click: showOpenDialog,
accelerator: 'CmdOrCtrl+O',
accelerator: 'CmdOrCtrl+O'
},
{
type: 'separator',
type: 'separator'
},
{
label: 'Save',
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE),
accelerator: 'CmdOrCtrl+S',
accelerator: 'CmdOrCtrl+S'
},
{
label: 'Save as',
click: () => showSaveDialog(IpcEvents.FS_SAVE_FIDDLE),
accelerator: 'CmdOrCtrl+Shift+S',
accelerator: 'CmdOrCtrl+Shift+S'
},
{
type: 'separator',
type: 'separator'
},
{
label: 'Publish to Gist',
@@ -224,24 +211,18 @@ function getFileMenu(): MenuItemConstructorOptions {
},
{
label: 'Save as Forge Project',
click: () =>
showSaveDialog(IpcEvents.FS_SAVE_FIDDLE_FORGE, 'Forge Project'),
},
click: () => showSaveDialog(IpcEvents.FS_SAVE_FIDDLE_FORGE, 'Forge Project')
}
];
// macOS has these items in the "Fiddle" menu
if (process.platform !== 'darwin') {
fileMenu.splice(
fileMenu.length,
0,
...getPreferencesItems(),
...getQuitItems(),
);
fileMenu.splice(fileMenu.length, 0, ...getPreferencesItems(), ...getQuitItems());
}
return {
label: 'File',
submenu: fileMenu,
submenu: fileMenu
};
}
@@ -251,85 +232,51 @@ function getFileMenu(): MenuItemConstructorOptions {
export function setupMenu() {
// Get template for default menu
const defaultMenu = require('electron-default-menu');
const menu = (defaultMenu(app, shell) as Array<
MenuItemConstructorOptions
>).map((item) => {
const { label } = item;
const menu = (defaultMenu(app, shell) as Array<MenuItemConstructorOptions>)
.map((item) => {
const { label } = item;
// Append the "Settings" item
if (
process.platform === 'darwin' &&
label === app.name &&
isSubmenu(item.submenu)
) {
item.submenu.splice(2, 0, ...getPreferencesItems());
}
// Append the "Settings" item
if (
process.platform === 'darwin'
&& label === app.getName()
&& isSubmenu(item.submenu)
) {
item.submenu.splice(2, 0, ...getPreferencesItems());
}
// Custom handler for "Select All" for Monaco
if (label === 'Edit' && isSubmenu(item.submenu)) {
const selectAll = item.submenu.find((i) => i.label === 'Select All')!;
delete selectAll.role; // override default role
selectAll.click = () => {
ipcMainManager.send(IpcEvents.SELECT_ALL_IN_EDITOR);
// Allow selection to occur in text fields outside the editors.
if (process.platform === 'darwin') {
Menu.sendActionToFirstResponder('selectAll:');
}
};
}
// Tweak "View" menu
if (label === 'View' && isSubmenu(item.submenu)) {
// 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' },
{
// 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
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Soft Wrap',
click: () =>
ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['wordWrap']),
},
);
item.submenu.push(
{ type: 'separator' },
{
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',
]),
},
);
item.submenu.push(
{ type: 'separator' },
{
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',
},
);
}
});
}
// Append items to "Help"
if (label === 'Help' && isSubmenu(item.submenu)) {
item.submenu = getHelpItems();
}
// Append items to "Help"
if (label === 'Help' && isSubmenu(item.submenu)) {
item.submenu = getHelpItems();
}
return item;
});
return item;
});
menu.splice(process.platform === 'darwin' ? 1 : 0, 0, getFileMenu());
menu.splice(
process.platform === 'darwin' ? 1 : 0,
0,
getFileMenu()
);
menu.splice(menu.length - 1, 0, getTasksMenu(), getShowMeMenu());
+11 -22
View File
@@ -8,11 +8,7 @@ import { isDevMode } from '../utils/devmode';
import { ipcMainManager } from './ipc';
const PROTOCOL = 'electron-fiddle';
const squirrelPath = path.resolve(
path.dirname(process.execPath),
'..',
'electron-fiddle.exe',
);
const squirrelPath = path.resolve(path.dirname(process.execPath), '..', 'electron-fiddle.exe');
const handlePotentialProtocolLaunch = (url: string) => {
if (!app.isReady()) {
@@ -30,18 +26,14 @@ const handlePotentialProtocolLaunch = (url: string) => {
case 'gist':
if (pathParts.length === 1) {
// We only have a gist ID
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [
{
id: pathParts[0],
},
]);
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [{
id: pathParts[0],
}]);
} else if (pathParts.length === 2) {
// We have a gist owner and gist ID, we can ignore the owner
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [
{
id: pathParts[1],
},
]);
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [{
id: pathParts[1],
}]);
} else {
// This is a super invalid gist launch
return;
@@ -52,12 +44,10 @@ const handlePotentialProtocolLaunch = (url: string) => {
if (pathParts.length > 1) {
// First part of the commit HASH / ref / branch
// Rest is the path to the example
ipcMainManager.send(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, [
{
ref: pathParts[0],
path: pathParts.slice(1).join('/'),
},
]);
ipcMainManager.send(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, [{
ref: pathParts[0],
path: pathParts.slice(1).join('/'),
}]);
} else {
// This is an invalid electron launch
return;
@@ -86,7 +76,6 @@ 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);
+11 -29
View File
@@ -1,11 +1,9 @@
import { app, BrowserWindow, shell } from 'electron';
import { IpcEvents } from '../ipc-events';
import { BrowserWindow, shell } from 'electron';
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.
export let browserWindows: Array<BrowserWindow | null> = [];
export let browserWindows: Array<BrowserWindow> = [];
/**
* Gets default options for the main window
@@ -23,11 +21,12 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
backgroundColor: '#1d2427',
webPreferences: {
webviewTag: false,
nodeIntegration: true,
},
nodeIntegration: true
}
};
}
/**
* Creates a new main window.
*
@@ -36,22 +35,20 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
*/
export function createMainWindow(): Electron.BrowserWindow {
console.log(`Creating main window`);
let browserWindow: BrowserWindow | null;
browserWindow = new BrowserWindow(getMainWindowOptions());
const browserWindow = new BrowserWindow(getMainWindowOptions());
browserWindow.loadFile('./dist/static/index.html');
browserWindow.webContents.once('dom-ready', () => {
if (browserWindow) {
browserWindow.show();
browserWindow.show();
if (browserWindow) {
createContextMenu(browserWindow);
}
});
browserWindow.on('closed', () => {
browserWindows = browserWindows.filter((bw) => browserWindow !== bw);
browserWindow = null;
browserWindows = browserWindows
.filter((bw) => browserWindow !== bw);
});
browserWindow.webContents.on('new-window', (event, url) => {
@@ -64,19 +61,6 @@ export function createMainWindow(): Electron.BrowserWindow {
shell.openExternal(url);
});
ipcMainManager.on(IpcEvents.SHOW_INACTIVE, () => {
if (browserWindow) {
browserWindow.showInactive();
}
});
const appData = app.getPath('appData');
ipcMainManager.send(
IpcEvents.SET_APPDATA_DIR,
[appData],
browserWindow.webContents,
);
browserWindows.push(browserWindow);
return browserWindow;
@@ -88,7 +72,5 @@ export function createMainWindow(): Electron.BrowserWindow {
* @returns {Electron.BrowserWindow}
*/
export function getOrCreateMainWindow(): Electron.BrowserWindow {
return (
BrowserWindow.getFocusedWindow() || browserWindows[0] || createMainWindow()
);
return BrowserWindow.getFocusedWindow() || browserWindows[0] || createMainWindow();
}
+41 -43
View File
@@ -1,6 +1,3 @@
import { initSentry } from '../sentry';
initSentry();
import { when } from 'mobx';
import * as MonacoType from 'monaco-editor';
@@ -9,8 +6,7 @@ import {
ALL_EDITORS,
EditorId,
EditorValues,
GenericDialogType,
SetFiddleOptions,
SetFiddleOptions
} from '../interfaces';
import { WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { updateEditorLayout } from '../utils/editor-layout';
@@ -18,7 +14,6 @@ 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 { EMPTY_EDITOR_CONTENT } from './constants';
import { FileManager } from './file-manager';
import { RemoteLoader } from './remote-loader';
import { Runner } from './runner';
@@ -52,52 +47,42 @@ export class App {
public async replaceFiddle(
editorValues: Partial<EditorValues>,
{ filePath, gistId, templateName }: Partial<SetFiddleOptions>,
{ 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,
this.state.setWarningDialogTexts({
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes',
ok: 'Yes'
});
this.state.isGenericDialogShowing = true;
await when(() => !this.state.isGenericDialogShowing);
this.state.isWarningDialogShowing = true;
await when(() => !this.state.isWarningDialogShowing);
if (!this.state.genericDialogLastResult) {
if (!this.state.warningDialogLastResult) {
return false;
}
}
// display all editors that have content
const visibleEditors = [];
for (const id of Object.keys(editorValues)) {
const content = editorValues[id];
// if the gist content matches the empty file output, don't show it
if (!Object.values(EMPTY_EDITOR_CONTENT).includes(content)) {
visibleEditors.push(id as EditorId);
}
}
// 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
await this.state.setVisibleMosaics(visibleEditors);
await this.setEditorValues(editorValues);
this.state.isUnsaved = false;
document.title = getTitle(this.state);
this.setupUnsavedOnChangeListener();
return true;
}
/**
* Sets the contents of all editor panes.
* Sets the values on all three editors.
*
* @param {EditorValues} values
* @param {warn} warn - Should we warn before overwriting unsaved data?
*/
public async setEditorValues(values: Partial<EditorValues>): Promise<void> {
const { ElectronFiddle: fiddle } = window;
@@ -111,16 +96,9 @@ export class App {
const backup = this.state.closedPanels[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]!;
}
if (isEditorBackup(backup) && backup.model) {
// The editor does not exist, attempt to set it on the backup
backup.model.setValue(values[name]!);
} else if (editor && editor.setValue) {
// The editor exists, set the value directly
editor.setValue(values[name]!);
@@ -130,12 +108,12 @@ export class App {
}
/**
* Retrieves the contents of all editor panes.
* Gets the values on all three editors.
*
* @returns {EditorValues}
*/
public async getEditorValues(
options?: PackageJsonOptions,
options?: PackageJsonOptions
): Promise<EditorValues> {
const { ElectronFiddle: fiddle } = window;
@@ -144,11 +122,10 @@ export class App {
}
const values: EditorValues = {
css: getEditorValue(EditorId.css),
html: getEditorValue(EditorId.html),
main: getEditorValue(EditorId.main),
preload: getEditorValue(EditorId.preload),
renderer: getEditorValue(EditorId.renderer),
renderer: getEditorValue(EditorId.renderer)
};
if (options && options.include !== false) {
@@ -188,9 +165,30 @@ 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.
*
@@ -198,7 +196,7 @@ export class App {
*/
public async setupTheme(): Promise<void> {
const tag: HTMLStyleElement | null = document.querySelector(
'style#fiddle-theme',
'style#fiddle-theme'
);
const theme = await getTheme(this.state.theme);
+216 -255
View File
@@ -1,275 +1,236 @@
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';
import { getOfflineTypeDefinitionPath } from './fetch-types';
/**
* General setup, called with a version. Is called during construction
* to ensure that we always have or download at least one version.
* The binary manager takes care of downloading Electron versions
*
* @param {string} iVersion
* @returns {Promise<void>}
* @export
* @class BinaryManager
*/
export async function setupBinary(
appState: AppState,
iVersion: string,
): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
export class BinaryManager {
public state: Record<string, 'ready' | 'downloading'> = {};
await fs.mkdirp(getDownloadPath(version));
/**
* 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>}
*/
public async remove(iVersion: string): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
let isDeleted = false;
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 = 1) => {
try {
await func();
} catch (error) {
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);
// utility to re-run removal functions upon failure
// due to windows filesystem lockfile jank
const rerunner = async (func: () => Promise<void>, counter: number = 1) => {
try {
await func();
} catch (error) {
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);
}
}
}
};
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;
isDeleted = true;
}
};
const typeDefsCleaner = async () => {
await removeTypeDefsForVersion(version);
};
await rerunner(binaryCleaner);
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;
const binaryCleaner = async () => {
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;
isDeleted = true;
}
};
const typeDefsCleaner = async () => {
await this.removeTypeDefsForVersion(version);
};
await rerunner(binaryCleaner);
if (isDeleted) {
await rerunner(typeDefsCleaner);
}
}
/**
* 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);
}
}
console.log(`Binary: Unpacked!`);
process.noAsar = false;
return knownVersions;
} catch (error) {
console.warn(`Could not read known Electron versions`);
return [];
}
}
resolve();
/**
* 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);
}
public async 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;
}
}
}
/**
* 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;
process.noAsar = true;
const options = {
dir: extractPath,
};
extract(zipPath, options, (error: Error) => {
if (error) {
reject(error);
return;
}
console.log(`BinaryManager: Unpacked!`);
process.noAsar = false;
resolve();
});
});
});
}
interface Progress {
percent: number;
transferred: number;
total: number;
}
}
+4 -5
View File
@@ -1,12 +1,12 @@
import { RunnableVersion } from '../interfaces';
import { ElectronVersion } from '../interfaces';
export class Bisector {
public revList: Array<RunnableVersion>;
public revList: Array<ElectronVersion>;
public minRev: number;
public maxRev: number;
private pivot: number;
constructor(revList: Array<RunnableVersion>) {
constructor(revList: Array<ElectronVersion>) {
this.getCurrentVersion = this.getCurrentVersion.bind(this);
this.continue = this.continue.bind(this);
this.calculatePivot = this.calculatePivot.bind(this);
@@ -36,8 +36,7 @@ export class Bisector {
isBisectOver = true;
}
} else {
const downPivot =
Math.floor((this.pivot - this.minRev) / 2) + this.minRev;
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;
+3 -6
View File
@@ -12,10 +12,7 @@ export interface ChromeMacProps {
@observer
export class ChromeMac extends React.Component<ChromeMacProps> {
public handleDoubleClick = () => {
const doubleClickAction = remote.systemPreferences.getUserDefault(
'AppleActionOnDoubleClick',
'string',
);
const doubleClickAction = remote.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
const win = remote.getCurrentWindow();
if (doubleClickAction === 'Minimize') {
win.minimize();
@@ -26,13 +23,13 @@ export class ChromeMac extends React.Component<ChromeMacProps> {
win.unmaximize();
}
}
};
}
public render() {
if (process.platform !== 'darwin') return null;
return (
<div className="chrome drag" onDoubleClick={this.handleDoubleClick}>
<div className='chrome drag' onDoubleClick={this.handleDoubleClick}>
<small>{getTitle(this.props.appState)}</small>
</div>
);
@@ -1,414 +0,0 @@
import {
Button,
ButtonGroup,
IToastProps,
Menu,
MenuItem,
Popover,
Position,
Toaster,
} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { when } from 'mobx';
import {
EditorValues,
GistActionState,
GistActionType,
} from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
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 { EMPTY_EDITOR_CONTENT } from '../constants';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface GistActionButtonProps {
appState: AppState;
}
interface IGistActionButtonState {
readonly isUpdating: boolean;
readonly isDeleting: boolean;
readonly actionType: GistActionType;
}
/**
* The "publish" button takes care of logging you in.
*
* @export
* @class GistActionButton
* @extends {React.Component<GistActionButtonProps, GistActionButtonState>}
*/
@observer
export class GistActionButton extends React.Component<
GistActionButtonProps,
IGistActionButtonState
> {
public constructor(props: GistActionButtonProps) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.performGistAction = this.performGistAction.bind(this);
this.setPrivate = this.setPrivate.bind(this);
this.setPublic = this.setPublic.bind(this);
this.state = {
isUpdating: false,
isDeleting: false,
actionType: GistActionType.publish,
};
}
private toaster: Toaster;
private refHandlers = {
toaster: (ref: Toaster) => (this.toaster = ref),
};
public componentDidMount() {
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.
*
* If we're showing the authentication dialog, we wait for it
* to be closed again (or a GitHub token to show up) before
* we publish
*
* @returns {Promise<void>}
* @memberof GistActionButton
*/
public async handleClick(): Promise<void> {
const { appState } = this.props;
if (!appState.gitHubToken) {
appState.toggleAuthDialog();
}
// Wait for the dialog to be closed again
await when(() => !!appState.gitHubToken || !appState.isTokenDialogShowing);
if (appState.gitHubToken) {
return this.performGistAction();
}
}
/**
* Publish a new GitHub gist.
*/
public async handlePublish() {
const { appState } = this.props;
const octo = await getOctokit(this.props.appState);
const { gitHubPublishAsPublic } = this.props.appState;
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getEditorValues(options);
appState.activeGistAction = GistActionState.publishing;
try {
const gist = await octo.gists.create({
public: !!gitHubPublishAsPublic,
description: 'Electron Fiddle Gist',
files: this.gistFilesList(values) as any, // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
});
appState.gistId = gist.data.id;
appState.localPath = undefined;
console.log(`Publish Button: Publishing complete`, { gist });
this.renderToast({ message: 'Publishing completed successfully!' });
// Only set action type to update if publish completed successfully.
this.setActionType(GistActionType.update);
} catch (error) {
console.warn(`Could not publish gist`, { error });
const messageBoxOptions: Electron.MessageBoxOptions = {
message:
'Publishing Fiddle to GitHub failed. Are you connected to the Internet?',
detail: `GitHub encountered the following error: ${error.message}`,
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
appState.activeGistAction = GistActionState.none;
}
/**
* Update an existing GitHub gist.
*/
public async handleUpdate() {
const { appState } = this.props;
const octo = await getOctokit(this.props.appState);
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getEditorValues(options);
appState.activeGistAction = GistActionState.updating;
try {
const gist = await octo.gists.update({
gist_id: appState.gistId!,
files: this.gistFilesList(values) as any,
});
console.log('Updating: Updating done', { gist });
this.renderToast({ message: 'Successfully updated gist!' });
} catch (error) {
console.warn(`Could not update gist`, { error });
const messageBoxOptions: Electron.MessageBoxOptions = {
message:
'Updating Fiddle Gist failed. Are you connected to the Internet and is this your Gist?',
detail: `GitHub encountered the following error: ${error.message}`,
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
appState.activeGistAction = GistActionState.none;
this.setActionType(GistActionType.update);
}
/**
* Delete an existing GitHub gist.
*/
public async handleDelete() {
const { appState } = this.props;
const octo = await getOctokit(this.props.appState);
appState.activeGistAction = GistActionState.deleting;
try {
const gist = await octo.gists.delete({
gist_id: appState.gistId!,
});
console.log('Deleting: Deleting done', { gist });
this.renderToast({ message: 'Successfully deleted gist!' });
} catch (error) {
console.warn(`Could not delete gist`, { error });
const messageBoxOptions: Electron.MessageBoxOptions = {
message:
'Deleting Fiddle Gist failed. Are you connected to the Internet, is this your Gist, and have you loaded it?',
detail: `GitHub encountered the following error: ${error.message}`,
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
appState.gistId = undefined;
appState.activeGistAction = GistActionState.none;
this.setActionType(GistActionType.publish);
}
/**
* Connect with GitHub, perform a publish/update/delete action,
* and update all related properties in the app state.
*/
public async performGistAction(): Promise<void> {
const { gistId } = this.props.appState;
const { actionType } = this.state;
if (gistId) {
switch (actionType) {
case GistActionType.publish:
await this.handlePublish();
break;
case GistActionType.update:
await this.handleUpdate();
break;
case GistActionType.delete:
await this.handleDelete();
break;
}
} else {
await this.handlePublish();
}
}
/**
* Publish fiddles as private.
*
* @memberof GistActionButton
*/
public setPrivate() {
this.setPrivacy(false);
}
/**
* Publish fiddles as public.
*
* @memberof GistActionButton
*/
public setPublic() {
this.setPrivacy(true);
}
public render() {
const { gistId, activeGistAction } = this.props.appState;
const { actionType } = this.state;
const getTextForButton = () => {
let text;
if (gistId) {
text = actionType;
} else if (activeGistAction === GistActionState.updating) {
text = 'Updating...';
} else if (activeGistAction === GistActionState.publishing) {
text = 'Publishing...';
} else if (activeGistAction === GistActionState.deleting) {
text = 'Deleting...';
} else {
text = 'Publish';
}
return text;
};
const getActionIcon = () => {
switch (actionType) {
case GistActionType.publish:
return 'upload';
case GistActionType.update:
return 'refresh';
case GistActionType.delete:
return 'delete';
}
};
const isPerformingAction = activeGistAction !== GistActionState.none;
return (
<>
<fieldset disabled={isPerformingAction}>
<ButtonGroup className="button-gist-action">
{this.renderPrivacyMenu()}
<Button
onClick={this.handleClick}
loading={isPerformingAction}
icon={getActionIcon()}
text={getTextForButton()}
/>
{this.renderGistActionMenu()}
</ButtonGroup>
</fieldset>
<Toaster
position={Position.BOTTOM_RIGHT}
ref={this.refHandlers.toaster}
/>
</>
);
}
private renderGistActionMenu = () => {
const { gistId } = this.props.appState;
const { actionType } = this.state;
if (!gistId) {
return null;
}
const menu = (
<Menu>
<MenuItem
text="Publish"
active={actionType === GistActionType.publish}
onClick={() => this.setActionType(GistActionType.publish)}
/>
<MenuItem
text="Update"
active={actionType === GistActionType.update}
onClick={() => this.setActionType(GistActionType.update)}
/>
<MenuItem
text="Delete"
active={actionType === GistActionType.delete}
onClick={() => this.setActionType(GistActionType.delete)}
/>
</Menu>
);
return (
<Popover content={menu} position={Position.BOTTOM}>
<Button icon="wrench" />
</Popover>
);
};
private renderPrivacyMenu = () => {
const { gitHubPublishAsPublic, gistId } = this.props.appState;
if (gistId) {
return null;
}
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
const privacyMenu = (
<Menu>
<MenuItem
text="Private"
icon="lock"
active={!gitHubPublishAsPublic}
onClick={this.setPrivate}
/>
<MenuItem
text="Public"
icon="unlock"
active={gitHubPublishAsPublic}
onClick={this.setPublic}
/>
</Menu>
);
return (
<Popover content={privacyMenu} position={Position.BOTTOM}>
<Button icon={privacyIcon} />
</Popover>
);
};
private setActionType = (actionType: GistActionType) => {
this.setState({ actionType });
};
private setPrivacy(publishAsPublic: boolean) {
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
}
private renderToast = (toast: IToastProps) => {
this.toaster.show(toast);
};
private gistFilesList = (values: EditorValues) => {
return {
[INDEX_HTML_NAME]: {
content: values.html || EMPTY_EDITOR_CONTENT.html,
},
[MAIN_JS_NAME]: {
content: values.main || EMPTY_EDITOR_CONTENT.js,
},
[RENDERER_JS_NAME]: {
content: values.renderer || EMPTY_EDITOR_CONTENT.js,
},
[PRELOAD_JS_NAME]: {
content: values.preload || EMPTY_EDITOR_CONTENT.js,
},
[STYLES_CSS_NAME]: {
content: values.css || EMPTY_EDITOR_CONTENT.css,
},
};
};
}
@@ -5,7 +5,6 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { IpcEvents } from '../../ipc-events';
import { GistActionState } from '../../interfaces';
import { idFromUrl, urlFromId } from '../../utils/gist';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
@@ -23,10 +22,7 @@ export interface AddressBarState {
}
@observer
export class AddressBar extends React.Component<
AddressBarProps,
AddressBarState
> {
export class AddressBar extends React.Component<AddressBarProps, AddressBarState> {
constructor(props: AddressBarProps) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
@@ -34,7 +30,7 @@ export class AddressBar extends React.Component<
this.submit = this.submit.bind(this);
const { gistId } = this.props.appState;
const value = urlFromId(gistId);
const value = gistId ? urlFromId(gistId) : '';
const { remoteLoader } = window.ElectronFiddle.app;
@@ -42,8 +38,8 @@ export class AddressBar extends React.Component<
value,
loaders: {
gist: remoteLoader.loadFiddleFromGist.bind(remoteLoader),
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader),
},
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader)
}
};
}
@@ -66,9 +62,7 @@ export class AddressBar extends React.Component<
public submit() {
const { remoteLoader } = window.ElectronFiddle.app;
if (this.state.value) {
remoteLoader.fetchGistAndLoad(
idFromUrl(this.state.value) || this.state.value,
);
remoteLoader.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
}
}
@@ -80,25 +74,16 @@ export class AddressBar extends React.Component<
const { loaders } = this.state;
reaction(
() => appState.gistId,
(gistId: string) => this.setState({ value: urlFromId(gistId) }),
(gistId: string) => this.setState({ value: urlFromId(gistId) })
);
ipcRendererManager.on(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.on(
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
loaders.example,
);
ipcRendererManager.on(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
public componentWillUnmount() {
const { loaders } = this.state;
ipcRendererManager.removeListener(
IpcEvents.LOAD_GIST_REQUEST,
loaders.gist,
);
ipcRendererManager.removeListener(
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
loaders.example,
);
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
/**
@@ -114,29 +99,28 @@ export class AddressBar extends React.Component<
return (
<Button
disabled={!isValueCorrect}
icon="cloud-download"
text="Load Fiddle"
icon='cloud-download'
text='Load Fiddle'
onClick={this.submit}
/>
);
}
public render() {
const { isUnsaved, activeGistAction } = 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 });
const isPerformingAction = activeGistAction !== GistActionState.none;
return (
<form className={className} onSubmit={this.handleSubmit}>
<fieldset disabled={isPerformingAction}>
<fieldset disabled={isPublishing}>
<InputGroup
key="addressbar"
leftIcon="geosearch"
key='addressbar'
leftIcon='geosearch'
intent={isCorrect || !value ? undefined : Intent.DANGER}
onChange={this.handleChange}
placeholder="https://gist.github.com/..."
placeholder='https://gist.github.com/...'
value={value}
rightElement={this.renderLoadButton(isCorrect)}
/>
+7 -34
View File
@@ -2,7 +2,6 @@ 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 {
@@ -19,37 +18,12 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
}
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 label = (
<>
Bisect complete. Check the range{' '}
<a
target="_blank"
rel="noreferrer"
href={`https://github.com/electron/electron/compare/v${minVer}...v${maxVer}`}
>
{minVer}...{maxVer}
</a>
.
</>
);
appState.pushOutput(`[BISECT] Complete: ${minVer}...${maxVer}`);
appState.setGenericDialogOptions({
type: GenericDialogType.success,
label,
cancel: undefined,
});
appState.isGenericDialogShowing = true;
appState.pushOutput(`[BISECT] Complete: Check between versions ${minRev.version} and ${maxRev.version}.`);
} else {
appState.setVersion(response.version);
}
@@ -63,21 +37,20 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
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}>
<Button
icon={'cross'}
onClick={this.terminateBisect}
>
Cancel Bisect
</Button>
</>
@@ -85,8 +58,8 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
} else {
return (
<Button
icon="git-branch"
text="Bisect"
icon='git-branch'
text='Bisect'
onClick={appState.toggleBisectDialog}
/>
);
+11 -30
View File
@@ -1,11 +1,4 @@
import {
Button,
Menu,
MenuDivider,
MenuItem,
Popover,
Position,
} from '@blueprintjs/core';
import { Button, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -29,10 +22,7 @@ export interface EditorDropdownProps {
* @extends {React.Component<EditorDropdownProps, EditorDropdownState>}
*/
@observer
export class EditorDropdown extends React.Component<
EditorDropdownProps,
EditorDropdownState
> {
export class EditorDropdown extends React.Component<EditorDropdownProps, EditorDropdownState> {
constructor(props: EditorDropdownProps) {
super(props);
@@ -43,7 +33,7 @@ export class EditorDropdown extends React.Component<
return (
<>
<Popover content={this.renderMenu()} position={Position.BOTTOM}>
<Button icon="applications" text="Editors" />
<Button icon='applications' text='Editors' />
</Popover>
{this.renderDocsDemos()}
</>
@@ -57,8 +47,8 @@ export class EditorDropdown extends React.Component<
return (
<Button
icon="help"
text="Docs & Demos"
icon='help'
text='Docs & Demos'
id={PanelId.docsDemo}
onClick={this.onItemClick}
active={!this.props.appState.closedPanels.docsDemo}
@@ -67,7 +57,11 @@ export class EditorDropdown extends React.Component<
}
public renderMenu() {
return <Menu>{...this.renderMenuItems()}</Menu>;
return (
<Menu>
{...this.renderMenuItems()}
</Menu>
);
}
public renderMenuItems() {
@@ -83,23 +77,10 @@ export class EditorDropdown extends React.Component<
text={TITLE_MAP[id]}
id={id}
onClick={this.onItemClick}
disabled={appState.mosaicArrangement === id} // can't hide last editor panel
/>,
/>
);
}
result.push(
<React.Fragment key={'fragment-reset-layout'}>
<MenuDivider />
<MenuItem
icon="grid-view"
key="reset-layout"
text="Reset Layout"
onClick={appState.resetEditorLayout}
/>
</React.Fragment>,
);
return result;
}
@@ -0,0 +1,172 @@
import { Button, ButtonGroup, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, ElectronVersionSource, VersionState } from '../../interfaces';
import { highlightText } from '../../utils/highlight-text';
import { sortedElectronMap } from '../../utils/sorted-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 === 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 {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;
}
export interface VersionChooserProps {
appState: AppState;
}
export const getVersionsFromAppState = (appState: AppState) => {
const { versions, versionsToShow, statesToShow } = 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;
});
};
/**
* 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 ElectronVersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
constructor(props: VersionChooserProps) {
super(props);
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 render() {
const { currentElectronVersion, Bisector } = this.props.appState;
const { version } = currentElectronVersion;
return (
<ButtonGroup>
<ElectronVersionSelect
filterable={true}
items={getVersionsFromAppState(this.props.appState)}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!!Bisector}
>
<Button
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentElectronVersion)}
disabled={!!Bisector}
/>
</ElectronVersionSelect>
</ButtonGroup>
);
}
}
@@ -0,0 +1,151 @@
import { Button, ButtonGroup, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, NodeVersion, VersionState } from '../../interfaces';
import { highlightText } from '../../utils/highlight-text';
import { sortedNodeMap } from '../../utils/sorted-map';
import { AppState } from '../state';
const ElectronVersionSelect = Select.ofType<NodeVersion>();
/**
* Helper method: Returns the <Select /> label for an Electron
* version.
*
* @param {ElectronVersion} { source, state }
* @returns {string}
*/
export function getItemLabel({ state }: NodeVersion): string {
let label = '';
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 {ElectronVersion} { state }
* @returns
*/
export function getItemIcon({ state }: NodeVersion) {
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<NodeVersion> = (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<NodeVersion> = (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;
}
export interface VersionChooserProps {
appState: AppState;
}
export const getVersionsFromAppState = (appState: AppState) => {
const { nodeVersions } = appState;
return sortedNodeMap<NodeVersion>(nodeVersions, (_key, item) => item)
.filter((item) => !!item);
};
/**
* 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 NodeVersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
constructor(props: VersionChooserProps) {
super(props);
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.setNodeVersion(version);
}
public render() {
const { currentNodeVersion, Bisector } = this.props.appState;
const { version } = currentNodeVersion;
return (
<ButtonGroup>
<ElectronVersionSelect
filterable={true}
items={getVersionsFromAppState(this.props.appState)}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!!Bisector}
>
<Button
className='version-chooser'
text={`Node v${version}`}
icon={getItemIcon(currentNodeVersion)}
disabled={!!Bisector}
/>
</ElectronVersionSelect>
</ButtonGroup>
);
}
}
@@ -0,0 +1,175 @@
import { Button, ButtonGroup, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
import { observer } from 'mobx-react';
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 { getOctokit } from '../../utils/octokit';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface PublishButtonProps {
appState: AppState;
}
/**
* The "publish" button takes care of logging you in.
*
* @export
* @class PublishButton
* @extends {React.Component<PublishButtonProps, PublishButtonState>}
*/
@observer
export class PublishButton extends React.Component<PublishButtonProps> {
constructor(props: PublishButtonProps) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.publishFiddle = this.publishFiddle.bind(this);
this.setPrivate = this.setPrivate.bind(this);
this.setPublic = this.setPublic.bind(this);
}
public componentDidMount() {
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
/**
* When the user clicks the publish button, we either show the
* authentication dialog or publish right away.
*
* If we're showing the authentication dialog, we wait for it
* to be closed again (or a GitHub token to show up) before
* we publish
*
* @returns {Promise<void>}
* @memberof PublishButton
*/
public async handleClick(): Promise<void> {
const { appState } = this.props;
if (!appState.gitHubToken) {
appState.toggleAuthDialog();
}
// Wait for the dialog to be closed again
await when(() => !!appState.gitHubToken || !appState.isTokenDialogShowing);
if (appState.gitHubToken) {
return this.publishFiddle();
}
}
/**
* Connect with GitHub, publish the current Fiddle as a gist,
* and update all related properties in the app state.
*/
public async publishFiddle(): Promise<void> {
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.getEditorValues(options);
try {
const gist = await octo.gists.create({
public: !!gitHubPublishAsPublic,
description: 'Electron Fiddle Gist',
files: {
[INDEX_HTML_NAME]: {
content: values.html || '<!-- Empty -->',
},
[MAIN_JS_NAME]: {
content: values.main || '// Empty',
},
[RENDERER_JS_NAME]: {
content: values.renderer || '// Empty',
},
},
} as any); // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
appState.gistId = gist.data.id;
console.log(`Publish Button: Publishing done`, { gist });
} catch (error) {
console.warn(`Could not publish gist`, { error });
const messageBoxOptions: Electron.MessageBoxOptions = {
message: 'Publishing Fiddle to GitHub failed. Are you connected to the Internet?',
detail: `GitHub encountered the following error: ${error.message}`
};
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
appState.isPublishing = false;
}
/**
* Publish fiddles as private.
*
* @memberof PublishButton
*/
public setPrivate() {
this.setPrivacy(false);
}
/**
* Publish fiddles as public.
*
* @memberof PublishButton
*/
public setPublic() {
this.setPrivacy(true);
}
public render() {
const { gitHubPublishAsPublic } = this.props.appState;
const { isPublishing } = this.props.appState;
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
const privacyMenu = (
<Menu>
<MenuItem
text='Private'
icon='lock'
active={!gitHubPublishAsPublic}
onClick={this.setPrivate}
/>
<MenuItem
text='Public'
icon='unlock'
active={gitHubPublishAsPublic}
onClick={this.setPublic}
/>
</Menu>
);
return (
<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>
);
}
private setPrivacy(publishAsPublic: boolean) {
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
}
}
+8 -10
View File
@@ -5,6 +5,9 @@ import * as React from 'react';
import { VersionState } from '../../interfaces';
import { AppState } from '../state';
export interface RunnerState {
}
export interface RunnerProps {
appState: AppState;
}
@@ -14,27 +17,21 @@ export interface RunnerProps {
* with Electron. It also renders the button that does so.
*
* @class Runner
* @extends {React.Component<RunnerProps>}
* @extends {React.Component<RunnerProps, RunnerState>}
*/
@observer
export class Runner extends React.Component<RunnerProps> {
export class Runner extends React.Component<RunnerProps, RunnerState> {
public render() {
const { isRunning, currentElectronVersion } = this.props.appState;
const state = currentElectronVersion && currentElectronVersion.state;
const props: IButtonProps = { className: 'button-run', disabled: true };
const props: IButtonProps = { className: 'button-run' };
if (state === VersionState.downloading) {
props.text = 'Downloading';
props.icon = (
<Spinner size={16} value={currentElectronVersion?.downloadProgress} />
);
} else if (state === VersionState.unzipping) {
props.text = 'Unzipping';
props.disabled = true;
props.icon = <Spinner size={16} />;
} else if (state === VersionState.ready) {
props.disabled = false;
if (isRunning) {
props.active = true;
props.text = 'Stop';
@@ -47,6 +44,7 @@ export class Runner extends React.Component<RunnerProps> {
}
} else {
props.text = 'Checking status';
props.disabled = true;
props.icon = <Spinner size={16} />;
}
@@ -1,29 +0,0 @@
import { ButtonGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
import { VersionSelect } from './version-select';
export interface VersionChooserProps {
appState: AppState;
}
/**
* A dropdown allowing the selection of Electron versions. The actual
* download is managed in the state.
*/
export const VersionChooser = observer((props: VersionChooserProps) => {
const { currentElectronVersion, Bisector, setVersion } = props.appState;
return (
<ButtonGroup>
<VersionSelect
appState={props.appState}
onVersionSelect={({ version }) => setVersion(version)}
currentVersion={currentElectronVersion}
disabled={!!Bisector}
/>
</ButtonGroup>
);
});
+35 -14
View File
@@ -6,9 +6,10 @@ import { AppState } from '../state';
import { AddressBar } from './commands-address-bar';
import { BisectHandler } from './commands-bisect';
import { EditorDropdown } from './commands-editors';
import { GistActionButton } from './commands-action-button';
import { ElectronVersionChooser } from './commands-electron-chooser';
import { NodeVersionChooser } from './commands-node-chooser';
import { PublishButton } from './commands-publish-button';
import { Runner } from './commands-runner';
import { VersionChooser } from './commands-version-chooser';
export interface CommandsProps {
appState: AppState;
@@ -19,10 +20,10 @@ export interface CommandsProps {
* all the things
*
* @class Commands
* @extends {React.Component<CommandsProps>}
* @extends {React.Component<CommandsProps, {}>}
*/
@observer
export class Commands extends React.Component<CommandsProps> {
export class Commands extends React.Component<CommandsProps, {}> {
constructor(props: CommandsProps) {
super(props);
}
@@ -32,32 +33,52 @@ export class Commands extends React.Component<CommandsProps> {
const { isBisectCommandShowing: isBisectCommandShowing } = appState;
return (
<div className="commands">
<div className='commands'>
<div>
<ControlGroup fill={true} vertical={false}>
<VersionChooser appState={appState} />
{this.getVersionChooser()}
<Runner appState={appState} />
</ControlGroup>
{isBisectCommandShowing && (
<ControlGroup fill={true} vertical={false}>
<BisectHandler 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}
icon="console"
text="Console"
icon='console'
text='Console'
onClick={appState.toggleConsole}
/>
<Button
active={appState.isNodeMode}
icon='ninja'
text='Node.js Mode'
onClick={appState.toggleNodeMode}
/>
<EditorDropdown appState={appState} />
</ControlGroup>
</div>
<div>
<AddressBar appState={appState} />
<GistActionButton appState={appState} />
<PublishButton appState={appState} />
</div>
</div>
);
}
public getVersionChooser() {
const { appState } = this.props;
if (appState.isNodeMode) {
return <NodeVersionChooser appState={appState} />;
} else {
return <ElectronVersionChooser appState={appState} />;
}
}
}
+38 -46
View File
@@ -7,7 +7,6 @@ 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';
@@ -26,10 +25,7 @@ export interface AddThemeDialogState {
* @extends {React.Component<AddThemeDialogProps, AddThemeDialogState>}
*/
@observer
export class AddThemeDialog extends React.Component<
AddThemeDialogProps,
AddThemeDialogState
> {
export class AddThemeDialog extends React.Component<AddThemeDialogProps, AddThemeDialogState> {
public resetState = { file: undefined };
constructor(props: AddThemeDialogProps) {
@@ -61,27 +57,19 @@ export class AddThemeDialog extends React.Component<
*/
public async onSubmit(): Promise<void> {
const { file } = this.state;
const defaultTheme = !!this.props.appState.theme
? await getTheme(this.props.appState.theme)
: defaultDark;
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
>;
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;
this.props.appState.setWarningDialogTexts({label: `Error: ${error}, please pick a different file.`});
this.props.appState.isWarningDialogShowing = true;
return;
}
@@ -89,39 +77,40 @@ export class AddThemeDialog extends React.Component<
return;
}
public async createNewThemeFromMonaco(
name: string,
newTheme: LoadedFiddleTheme,
): Promise<boolean> {
if (!name) return false;
const themePath = path.join(THEMES_PATH, `${name}`);
public async createNewThemeFromMonaco(name: string, newTheme: LoadedFiddleTheme): Promise<boolean> {
if (!name) return false;
const themePath = path.join(THEMES_PATH, `${name}`);
await fsType.outputJSON(
themePath,
{
await fsType.outputJSON(themePath, {
...newTheme,
name,
},
{ spaces: 2 },
);
}, {spaces: 2});
this.props.appState.setTheme(themePath);
shell.showItemInFolder(themePath);
return true;
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" />,
(
<Button
icon='add'
key='submit'
disabled={!canSubmit}
onClick={this.onSubmit}
text='Add'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
@@ -140,10 +129,10 @@ export class AddThemeDialog extends React.Component<
<Dialog
isOpen={isThemeDialogShowing}
onClose={this.onClose}
title="Add theme"
className="dialog-add-version"
title='Add theme'
className='dialog-add-version'
>
<div className="bp3-dialog-body">
<div className='bp3-dialog-body'>
<FileInput
onInputChange={this.onChangeFile}
inputProps={inputProps as any}
@@ -151,8 +140,10 @@ export class AddThemeDialog extends React.Component<
/>
<br />
</div>
<div className="bp3-dialog-footer">
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
@@ -165,4 +156,5 @@ export class AddThemeDialog extends React.Component<
this.setState(this.resetState);
return;
}
}
+47 -46
View File
@@ -1,20 +1,13 @@
import {
Button,
Callout,
Dialog,
FileInput,
InputGroup,
Intent,
} from '@blueprintjs/core';
import { Button, Callout, Dialog, FileInput, InputGroup, Intent } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as path from 'path';
import * as React from 'react';
import * as semver from 'semver';
import { Version } from '../../interfaces';
import { NpmVersion } 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';
@@ -36,29 +29,23 @@ export interface AddVersionDialogState {
* @extends {React.Component<AddVersionDialogProps, AddVersionDialogState>}
*/
@observer
export class AddVersionDialog extends React.Component<
AddVersionDialogProps,
AddVersionDialogState
> {
export class AddVersionDialog extends React.Component<AddVersionDialogProps, AddVersionDialogState> {
constructor(props: AddVersionDialogProps) {
super(props);
this.state = {
isValidVersion: false,
isValidElectron: false,
version: '',
version: ''
};
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onChangeVersion = this.onChangeVersion.bind(this);
ipcRendererManager.on(
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
(_event, [file]) => {
this.setFolderPath(file);
},
);
ipcRendererManager.on(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, (_event, [file]) => {
this.setFolderPath(file);
});
}
public componentWillUnmount() {
@@ -71,7 +58,8 @@ export class AddVersionDialog extends React.Component<
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public async setFolderPath(folderPath: string) {
const isValidElectron = !!(await getIsDownloaded('custom', folderPath));
const { binaryManager } = this.props.appState;
const isValidElectron = !!await binaryManager.getIsDownloaded('custom', folderPath);
this.setState({ folderPath, isValidElectron });
}
@@ -96,12 +84,16 @@ export class AddVersionDialog extends React.Component<
if (!folderPath) return;
const name = folderPath.slice(-20).split(path.sep).slice(1).join(path.sep);
const name = folderPath
.slice(-20)
.split(path.sep)
.slice(1)
.join(path.sep);
const toAdd: Version = {
const toAdd: NpmVersion = {
localPath: folderPath,
version,
name,
name
};
this.props.appState.addLocalVersion(toAdd);
@@ -120,14 +112,22 @@ export class AddVersionDialog extends React.Component<
const canSubmit = this.state.isValidElectron && this.state.isValidVersion;
return [
<Button
icon="add"
key="submit"
disabled={!canSubmit}
onClick={this.onSubmit}
text="Add"
/>,
<Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
(
<Button
icon='add'
key='submit'
disabled={!canSubmit}
onClick={this.onSubmit}
text='Add'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
@@ -137,32 +137,33 @@ export class AddVersionDialog extends React.Component<
onClick: (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
e.preventDefault();
ipcRendererManager.send(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG);
},
}
};
const { folderPath } = this.state;
const text =
folderPath ||
const text = folderPath ||
`Select the folder containing ${getElectronNameForPlatform()}...`;
return (
<Dialog
isOpen={isAddVersionDialogShowing}
onClose={this.onClose}
title="Add local Electron build"
className="dialog-add-version"
title='Add local Electron build'
className='dialog-add-version'
>
<div className="bp3-dialog-body">
<div className='bp3-dialog-body'>
<FileInput
id="custom-electron-version"
id='custom-electron-version'
inputProps={inputProps as any}
text={text}
/>
<br />
{this.renderPath()}
</div>
<div className="bp3-dialog-footer">
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
@@ -193,14 +194,14 @@ export class AddVersionDialog extends React.Component<
return (
<>
<p>
Please specify a version, used for typings and the name. Must be{' '}
<code>semver</code> compliant.
Please specify a version, used for typings and the name.
Must be <code>semver</code> compliant.
</p>
<InputGroup
intent={isValidVersion ? undefined : Intent.DANGER}
value={version}
onChange={this.onChangeVersion}
placeholder="4.0.0"
placeholder='4.0.0'
/>
</>
);
@@ -214,7 +215,7 @@ export class AddVersionDialog extends React.Component<
isValidElectron: false,
isValidVersion: false,
version: '',
folderPath: undefined,
folderPath: undefined
});
}
}
+76 -146
View File
@@ -1,34 +1,33 @@
import { Button, ButtonGroup, Callout, Dialog, Label } from '@blueprintjs/core';
import { Button, Dialog, Label, MenuItem } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { RunnableVersion } from '../../interfaces';
import { ElectronVersion } from '../../interfaces';
import { Bisector } from '../bisect';
import { AppState } from '../state';
import { VersionSelect } from './version-select';
import { filterItem, getItemIcon, getVersionsFromAppState, renderItem } from './commands-electron-chooser';
const ElectronVersionSelect = Select.ofType<ElectronVersion>();
export interface BisectDialogProps {
appState: AppState;
}
export interface BisectDialogState {
startIndex: number;
endIndex: number;
allVersions: Array<RunnableVersion>;
showHelp?: boolean;
startIndex?: number;
endIndex?: number;
allVersions: Array<ElectronVersion>;
}
/**
* The "add version" dialog allows users to add custom builds of Electron.
*
* @class BisectDialog
* @class AddVersionDialog
* @extends {React.Component<BisectDialogProps, BisectDialogState>}
*/
@observer
export class BisectDialog extends React.Component<
BisectDialogProps,
BisectDialogState
> {
export class BisectDialog extends React.Component<BisectDialogProps, BisectDialogState> {
constructor(props: BisectDialogProps) {
super(props);
@@ -36,22 +35,16 @@ export class BisectDialog extends React.Component<
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,
};
const allVersions = getVersionsFromAppState(this.props.appState);
this.state = { allVersions };
}
public onBeginSelect(version: RunnableVersion) {
public onBeginSelect(version: ElectronVersion) {
this.setState({ startIndex: this.state.allVersions.indexOf(version) });
}
public onEndSelect(version: RunnableVersion) {
public onEndSelect(version: ElectronVersion) {
this.setState({ endIndex: this.state.allVersions.indexOf(version) });
}
@@ -68,7 +61,9 @@ export class BisectDialog extends React.Component<
return;
}
const bisectRange = allVersions.slice(endIndex, startIndex + 1).reverse();
const bisectRange = allVersions
.slice(endIndex, startIndex + 1)
.reverse();
appState.Bisector = new Bisector(bisectRange);
const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
@@ -83,83 +78,32 @@ export class BisectDialog extends React.Component<
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() {
const canSubmit =
!!this.state.startIndex &&
!!this.state.endIndex &&
this.state.startIndex > this.state.endIndex;
return [
<Button
icon="play"
key="submit"
disabled={!this.canSubmit}
onClick={this.onSubmit}
text="Begin"
/>,
<Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
(
<Button
icon='play'
key='submit'
disabled={!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&apos;s the &quot;last known good&quot; version that did not
have the bug. Then, select that latest version to end the search
with, usually the &quot;first known bad&quot; 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 &quot;bisect&quot; is a popular method{' '}
<a
href="https://git-scm.com/docs/git-bisect"
target="_blank"
rel="noreferrer"
>
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;
@@ -168,67 +112,53 @@ export class BisectDialog extends React.Component<
<Dialog
isOpen={isBisectDialogShowing}
onClose={this.onClose}
title="Start a bisect session"
className="dialog-add-version"
title='Start a bisect session'
className='dialog-add-version'
>
<div className="bp3-dialog-body">
{this.help}
<div className='bp3-dialog-body'>
<Label>
Earliest Version (Last &quot;known good&quot; version)
<ButtonGroup fill={true}>
<VersionSelect
currentVersion={allVersions[startIndex]}
appState={this.props.appState}
onVersionSelect={this.onBeginSelect}
itemDisabled={this.isEarliestItemDisabled}
Earliest Version
<ElectronVersionSelect
filterable={true}
items={allVersions}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onBeginSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
>
<Button
text={startIndex ? `v${allVersions[startIndex].version}` : ``}
icon={startIndex ? getItemIcon(allVersions[startIndex]) : 'small-minus'}
fill={true}
/>
</ButtonGroup>
</ElectronVersionSelect>
</Label>
<Label>
Latest Version (First &quot;known bad&quot; version)
<ButtonGroup fill={true}>
<VersionSelect
currentVersion={allVersions[endIndex]}
appState={this.props.appState}
onVersionSelect={this.onEndSelect}
itemDisabled={this.isLatestItemDisabled}
Latest Version
<ElectronVersionSelect
filterable={true}
items={allVersions.slice(0, startIndex!)}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onEndSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!startIndex}
>
<Button
text={endIndex ? `v${allVersions[endIndex].version}` : ``}
icon={endIndex ? getItemIcon(allVersions[endIndex]) : 'small-minus'}
fill={true}
disabled={!startIndex}
/>
</ButtonGroup>
</ElectronVersionSelect>
</Label>
</div>
<div className="bp3-dialog-footer">
<div className="bp3-dialog-footer-actions">{this.buttons}</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,51 @@
import { Alert, Intent } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
export interface ConfirmDialogProps {
appState: AppState;
}
export interface ConfirmDialogState {
}
/**
* The token dialog prompts the user to either continue or cancel the operation.
*
* @export
* @class ConfirmDialog
* @extends {React.Component<ConfirmDialogProps, ConfirmDialogState>}
*/
@observer
export class ConfirmDialog extends React.Component<ConfirmDialogProps, ConfirmDialogState> {
constructor(props: ConfirmDialogProps) {
super(props);
this.onClose = this.onClose.bind(this);
}
public onClose(result: boolean) {
this.props.appState.confirmationPromptLastResult = result;
this.props.appState.toggleConfirmationPromptDialog();
}
public render() {
const { isConfirmationPromptShowing, confirmationDialogTexts } = this.props.appState;
return (
<Alert
isOpen={isConfirmationPromptShowing}
onClose={this.onClose}
icon='help'
confirmButtonText={confirmationDialogTexts.ok}
cancelButtonText={confirmationDialogTexts.cancel}
intent={Intent.PRIMARY}
>
<p>{confirmationDialogTexts.label}</p>
</Alert>
);
}
}
@@ -1,72 +0,0 @@
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;
}
/**
* 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> {
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>
);
}
}
+29 -31
View File
@@ -1,3 +1,4 @@
import { Button, Callout, Dialog, InputGroup, Intent } from '@blueprintjs/core';
import { clipboard, shell } from 'electron';
import { observer } from 'mobx-react';
@@ -16,7 +17,7 @@ export interface TokenDialogState {
error: boolean;
}
const TOKEN_SCOPES = ['gist'].join();
const TOKEN_SCOPES = [ 'gist' ].join();
const TOKEN_DESCRIPTION = encodeURIComponent('Fiddle Gist Token');
const GENERATE_TOKEN_URL = `https://github.com/settings/tokens/new?scopes=${TOKEN_SCOPES}&description=${TOKEN_DESCRIPTION}`;
@@ -29,17 +30,14 @@ const GENERATE_TOKEN_URL = `https://github.com/settings/tokens/new?scopes=${TOKE
* @extends {React.Component<TokenDialogProps, TokenDialogState>}
*/
@observer
export class TokenDialog extends React.Component<
TokenDialogProps,
TokenDialogState
> {
export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogState> {
constructor(props: TokenDialogProps) {
super(props);
this.state = {
verifying: false,
error: false,
tokenInput: '',
tokenInput: ''
};
this.onSubmitToken = this.onSubmitToken.bind(this);
@@ -71,7 +69,6 @@ export class TokenDialog extends React.Component<
} catch (error) {
console.warn(`Authenticating against GitHub failed`, error);
this.setState({ verifying: false, error: true });
this.props.appState.gitHubToken = null;
return;
}
@@ -93,7 +90,7 @@ export class TokenDialog extends React.Component<
this.setState({
verifying: false,
error: false,
tokenInput: '',
tokenInput: ''
});
}
@@ -136,20 +133,23 @@ export class TokenDialog extends React.Component<
const canSubmit = !!this.state.tokenInput;
return [
<Button
key="done"
disabled={!canSubmit}
onClick={this.onSubmitToken}
loading={this.state.verifying}
text="Done"
icon="log-in"
/>,
<Button
key="cancel"
text="Cancel"
icon="log-out"
onClick={this.onClose}
/>,
(
<Button
key='done'
disabled={!canSubmit}
onClick={this.onSubmitToken}
loading={this.state.verifying}
text='Done'
icon='log-in'
/>
), (
<Button
key='cancel'
text='Cancel'
icon='log-out'
onClick={this.onClose}
/>
)
];
}
@@ -171,15 +171,11 @@ export class TokenDialog extends React.Component<
<Dialog
isOpen={isTokenDialogShowing}
onClose={this.onClose}
title="GitHub Token"
title='GitHub Token'
>
<div className="bp3-dialog-body">
<div className='bp3-dialog-body'>
<p>
Generate a{' '}
<a onClick={this.openGenerateTokenExternal}>
GitHub Personal Access Token
</a>{' '}
and paste it here:
Generate a <a onClick={this.openGenerateTokenExternal}>GitHub Personal Access Token</a> and paste it here:
</p>
{this.state.error ? this.invalidWarning : null}
@@ -190,8 +186,10 @@ export class TokenDialog extends React.Component<
onChange={this.handleChange}
/>
</div>
<div className="bp3-dialog-footer">
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
@@ -0,0 +1,50 @@
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 prompts the user to either continue or cancel the operation.
*
* @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.toggleWarningDialog();
}
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>
);
}
}
+23 -24
View File
@@ -5,8 +5,9 @@ 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 { ConfirmDialog } from './dialog-confirm';
import { TokenDialog } from './dialog-token';
import { WarningDialog } from './dialog-warning';
import { Settings } from './settings';
export interface DialogsProps {
@@ -20,7 +21,7 @@ export interface DialogsProps {
* @extends {React.Component<DialogsProps, {}>}
*/
@observer
export class Dialogs extends React.Component<DialogsProps> {
export class Dialogs extends React.Component<DialogsProps, {}> {
public render() {
const { appState } = this.props;
const {
@@ -28,36 +29,34 @@ export class Dialogs extends React.Component<DialogsProps> {
isSettingsShowing,
isAddVersionDialogShowing,
isThemeDialogShowing,
isBisectDialogShowing,
isGenericDialogShowing,
isBisectDialogShowing
} = appState;
const maybeToken = isTokenDialogShowing ? (
<TokenDialog key="dialogs" appState={appState} />
) : null;
const maybeSettings = isSettingsShowing ? (
<Settings key="settings" appState={appState} />
) : null;
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;
const maybeToken = isTokenDialogShowing
? <TokenDialog key='dialogs' appState={appState} />
: null;
const maybeSettings = isSettingsShowing
? <Settings key='settings' appState={appState} />
: null;
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 eitherWarningOrPrompt = appState.isWarningDialogShowing
? <WarningDialog appState={appState} />
: <ConfirmDialog appState={appState} />;
return (
<div key="dialogs" className="dialogs">
<div key='dialogs' className='dialogs'>
{maybeToken}
{maybeSettings}
{maybeAddLocalVersion}
{maybeMonaco}
{maybeBisect}
{genericDialog}
{eitherWarningOrPrompt}
</div>
);
}
+17 -53
View File
@@ -15,41 +15,28 @@ export interface EditorProps {
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;
onChange?: (value: string, event: MonacoType.editor.IModelContentChangedEvent) => void;
}
export class Editor extends React.Component<EditorProps> {
public editor: MonacoType.editor.IStandaloneCodeEditor;
public language = 'javascript';
public value = '';
public language: string = 'javascript';
public value: string = '';
private containerRef = React.createRef<HTMLDivElement>();
constructor(props: EditorProps) {
super(props);
switch (props.id) {
case 'html':
this.language = 'html';
break;
case 'css':
this.language = 'css';
break;
default:
this.language = 'javascript';
}
this.language = props.id === 'html' ? 'html' : 'javascript';
}
public shouldComponentUpdate() {
return false;
}
public async componentDidMount() {
await this.initMonaco();
public componentDidMount() {
this.initMonaco();
}
public componentWillUnmount() {
@@ -90,13 +77,7 @@ export class Editor extends React.Component<EditorProps> {
theme: 'main',
contextmenu: false,
model: null,
...monacoOptions,
});
// mark this editor as focused whenever it is
this.editor.onDidFocusEditorText(() => {
const { id, setFocused } = this.props;
setFocused(id);
...monacoOptions
});
await this.editorDidMount(this.editor);
@@ -114,24 +95,7 @@ export class Editor extends React.Component<EditorProps> {
}
public render() {
return <div className="editorContainer" ref={this.containerRef} />;
}
/**
* Create a model and attach it to the editor
*
* @private
* @param {string} value
*/
private createModel(value: string) {
const { monaco } = this.props;
const model = monaco.editor.createModel(value, this.language);
model.updateOptions({
tabSize: 2,
});
this.editor.setModel(model);
return <div className='editorContainer' ref={this.containerRef} />;
}
/**
@@ -141,27 +105,27 @@ export class Editor extends React.Component<EditorProps> {
* @memberof Editor
*/
private async setContent() {
const { appState, id } = this.props;
const { appState, id, monaco } = this.props;
const { version } = appState;
const backup = appState.getAndRemoveEditorValueBackup(id);
if (backup) {
if (backup && backup.model) {
console.log(`Editor: Backup found, restoring state`);
if (backup.viewState) {
this.editor.restoreViewState(backup.viewState);
}
// 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 ?? '');
}
this.editor.setModel(backup.model);
} else {
const value = await getContent(id, version);
this.createModel(value);
const model = monaco.editor.createModel(value, this.language);
model.updateOptions({
tabSize: 2
});
this.editor.setModel(model);
}
}
}
@@ -6,10 +6,10 @@ import { EditorId } from '../../interfaces';
import { AppState } from '../state';
export function renderNonIdealState(appState: AppState) {
const allEditors = [EditorId.html, EditorId.main, EditorId.renderer];
const allEditors = [ EditorId.html, EditorId.main, EditorId.renderer ];
const resolveButton = (
<Button
text="Open all editors"
text='Open all editors'
onClick={() => appState.setVisibleMosaics(allEditors)}
/>
);
@@ -17,7 +17,7 @@ export function renderNonIdealState(appState: AppState) {
return (
<NonIdealState
action={resolveButton}
icon="applications"
icon='applications'
description='You have closed all editors. You can open them again with the button below or the "Editors" button above!'
/>
);
@@ -1,11 +1,7 @@
import * as React from 'react';
import { Button } from '@blueprintjs/core';
import {
MosaicContext,
MosaicRootActions,
MosaicWindowContext,
} from 'react-mosaic-component';
import { MosaicContext, MosaicRootActions, MosaicWindowContext } from 'react-mosaic-component';
import { DocsDemoPage, MosaicId } from '../../interfaces';
import { AppState } from '../state';
@@ -15,9 +11,7 @@ export interface ToolbarButtonProps {
id: MosaicId;
}
export abstract class ToolbarButton extends React.PureComponent<
ToolbarButtonProps
> {
export abstract class ToolbarButton extends React.PureComponent<ToolbarButtonProps> {
public static contextType = MosaicWindowContext;
public context: MosaicWindowContext;
@@ -36,9 +30,7 @@ export abstract class ToolbarButton extends React.PureComponent<
/**
* Create a button that performs the actual action
*/
public abstract createButton(
_mosaicActions: MosaicRootActions<any>,
): React.ReactNode;
public abstract createButton(_mosaicActions: MosaicRootActions<any>): React.ReactNode;
}
export class MaximizeButton extends ToolbarButton {
@@ -50,7 +42,13 @@ export class MaximizeButton extends ToolbarButton {
mosaicActions.expand(this.context.mosaicWindowActions.getPath());
};
return <Button icon="maximize" className="bp3-small" onClick={onClick} />;
return (
<Button
icon='maximize'
className='bp3-small'
onClick={onClick}
/>
);
}
}
@@ -59,10 +57,15 @@ 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);
const onClick = () => this.props.appState.hideAndBackupMosaic(this.props.id);
return <Button icon="cross" className="bp3-small" onClick={onClick} />;
return (
<Button
icon='cross'
className='bp3-small'
onClick={onClick}
/>
);
}
}
@@ -71,15 +74,14 @@ 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);
const onClick = () => this.props.appState.currentDocsDemoPage = DocsDemoPage.DEFAULT;
return (
<Button
icon="home"
className="bp3-small"
icon='home'
className='bp3-small'
onClick={onClick}
text="Overview"
text='Overview'
/>
);
}
+44 -106
View File
@@ -2,13 +2,7 @@ import { reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as MonacoType from 'monaco-editor';
import * as React from 'react';
import {
Mosaic,
MosaicBranch,
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';
@@ -23,18 +17,14 @@ import { AppState } from '../state';
import { activateTheme } from '../themes';
import { Editor } from './editor';
import { renderNonIdealState } from './editors-non-ideal-state';
import {
DocsDemoGoHomeButton,
MaximizeButton,
RemoveButton,
} from './editors-toolbar-button';
import { DocsDemoGoHomeButton, MaximizeButton, RemoveButton } from './editors-toolbar-button';
import { ShowMe } from './show-me';
const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
minimap: {
enabled: false,
enabled: false
},
wordWrap: 'on',
wordWrap: 'on'
};
export const TITLE_MAP: Record<MosaicId, string> = {
@@ -42,7 +32,6 @@ export const TITLE_MAP: Record<MosaicId, string> = {
renderer: 'Renderer Process (renderer.js)',
preload: 'Preload (preload.js)',
html: 'HTML (index.html)',
css: 'Stylesheet (styles.css)',
docsDemo: 'Docs & Demos',
};
@@ -54,7 +43,6 @@ export interface EditorsState {
monaco?: typeof MonacoType;
isMounted?: boolean;
monacoOptions: MonacoType.editor.IEditorOptions;
focused?: EditorId;
}
@observer
@@ -63,7 +51,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
// the editor layout. That method is itself debounced.
public disposeLayoutAutorun = reaction(
() => this.props.appState.mosaicArrangement,
() => updateEditorLayout(),
() => updateEditorLayout()
);
constructor(props: EditorsProps) {
@@ -73,9 +61,10 @@ 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();
}
/**
@@ -83,56 +72,30 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
*
* @memberof Editors
*/
public async componentDidMount() {
ipcRendererManager.on(
IpcEvents.MONACO_EXECUTE_COMMAND,
(_event, cmd: string) => {
this.executeCommand(cmd);
},
);
public componentDidMount() {
ipcRendererManager.on(IpcEvents.MONACO_EXECUTE_COMMAND, (_event, cmd: string) => {
this.executeCommand(cmd);
});
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
const { version } = this.props.appState;
await window.ElectronFiddle.app.replaceFiddle(
{
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) => {
this.toggleEditorOption(cmd);
},
);
ipcRendererManager.on(IpcEvents.SELECT_ALL_IN_EDITOR, (_event) => {
// programmatically fetch all editor contents and set as selection
const editor = getFocusedEditor();
const range = editor?.getModel()?.getFullModelRange();
if (!!range) {
editor?.setSelection(range);
}
ipcRendererManager.on(IpcEvents.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
this.toggleEditorOption(cmd);
});
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);
ipcRendererManager.removeAllListeners(IpcEvents.SELECT_ALL_IN_EDITOR);
}
/**
@@ -147,9 +110,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
if (editor) {
const command = editor.getAction(commandId);
console.log(
`Editors: Trying to run ${command.id}. Supported: ${command.isSupported}`,
);
console.log(`Editors: Trying to run ${command.id}. Supported: ${command.isSupported}`);
if (command && command.isSupported()) {
command.run();
@@ -169,14 +130,15 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
setAtPath(path, newOptions, toggleMonaco(currentSetting));
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
const editor: MonacoType.editor.IStandaloneCodeEditor | null =
window.ElectronFiddle.editors[key];
Object.keys(window.ElectronFiddle.editors)
.forEach((key) => {
const editor: MonacoType.editor.IStandaloneCodeEditor | null
= window.ElectronFiddle.editors[key];
if (editor) {
editor.updateOptions(newOptions);
}
});
if (editor) {
editor.updateOptions(newOptions);
}
});
this.setState({ monacoOptions: newOptions });
@@ -196,37 +158,27 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
* @returns {JSX.Element}
*/
public renderToolbar(
{ title }: MosaicWindowProps<MosaicId>,
id: MosaicId,
{ title }: MosaicWindowProps<MosaicId>, id: MosaicId
): JSX.Element {
const { appState } = this.props;
const docsDemoGoHomeMaybe =
id === PanelId.docsDemo ? (
<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} />
</>
);
const docsDemoGoHomeMaybe = id === PanelId.docsDemo
? <DocsDemoGoHomeButton id={id} appState={this.props.appState} />
: null;
return (
<div>
{/* Left */}
<div>
<h5>{title}</h5>
<h5>
{title}
</h5>
</div>
{/* Middle */}
<div />
{/* Right */}
<div className="mosaic-controls">
<div className='mosaic-controls'>
{docsDemoGoHomeMaybe}
{toolbarControlsMaybe}
<MaximizeButton id={id} appState={this.props.appState} />
<RemoveButton id={id} appState={this.props.appState} />
</div>
</div>
);
@@ -250,9 +202,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
className={id}
path={path}
title={TITLE_MAP[id]}
renderToolbar={(props: MosaicWindowProps<MosaicId>) =>
this.renderToolbar(props, id)
}
renderToolbar={(props: MosaicWindowProps<MosaicId>) => this.renderToolbar(props, id)}
>
{content}
</MosaicWindow>
@@ -287,7 +237,6 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
monaco={monaco!}
appState={appState}
monacoOptions={defaultMonacoOptions}
setFocused={this.setFocused}
/>
);
}
@@ -300,7 +249,6 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
return (
<Mosaic<EditorId | PanelId>
className={`focused__${this.state.focused}`}
onChange={this.onChange}
value={appState.mosaicArrangement}
zeroStateView={renderNonIdealState(appState)}
@@ -323,34 +271,24 @@ 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() {
public async loadMonaco(): Promise<void> {
const { app } = window.ElectronFiddle;
const loader = require('monaco-loader');
const monaco = app.monaco || (await loader());
const monaco = app.monaco || await loader();
if (!app.monaco) {
app.monaco = monaco;
}
if (!this.state || !this.state.isMounted) {
this.setState({
this.state = {
monaco,
monacoOptions: defaultMonacoOptions,
});
monacoOptions: defaultMonacoOptions
};
} else {
this.setState({ monaco });
}
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 });
activateTheme(monaco, undefined, this.props.appState.theme);
}
}
+4 -4
View File
@@ -13,15 +13,15 @@ export interface HeaderProps {
* Everything above the editors, so buttons and the address bar.
*
* @class Header
* @extends {React.Component<HeaderProps>}
* @extends {React.Component<HeaderProps, HeaderState>}
*/
export class Header extends React.Component<HeaderProps> {
export class Header extends React.Component<HeaderProps, {}> {
public render() {
return (
<>
<ChromeMac appState={this.props.appState} />
<header id="header">
<Commands key="commands" appState={this.props.appState} />
<header id='header'>
<Commands key='commands' appState={this.props.appState} />
</header>
<WelcomeTour appState={this.props.appState} />
</>
@@ -15,13 +15,11 @@ export interface WrapperState {
export type WrapperMosaicId = 'output' | 'editors';
export class OutputEditorsWrapper extends React.Component<
WrapperProps,
WrapperState
> {
export class OutputEditorsWrapper extends React.Component<WrapperProps, WrapperState> {
private MOSAIC_ELEMENTS = {
output: <Output appState={this.props.appState} />,
editors: <Editors appState={this.props.appState} />,
editors: <Editors appState={this.props.appState} />
};
constructor(props: any) {
@@ -32,7 +30,7 @@ export class OutputEditorsWrapper extends React.Component<
first: 'output',
second: 'editors',
splitPercentage: 25,
},
}
};
}
@@ -41,7 +39,7 @@ export class OutputEditorsWrapper extends React.Component<
<>
<Mosaic<WrapperMosaicId>
renderTile={(id: string) => this.MOSAIC_ELEMENTS[id]}
resize={{ minimumPaneSizePercentage: 0 }}
resize={{minimumPaneSizePercentage: 0}}
value={this.state.mosaicArrangement}
onChange={this.onChange}
/>
@@ -56,6 +54,6 @@ export class OutputEditorsWrapper extends React.Component<
this.props.appState.isConsoleShowing = isConsoleShowing;
}
this.setState({ mosaicArrangement: currentNode });
};
this.setState({mosaicArrangement: currentNode});
}
}
+8 -9
View File
@@ -7,6 +7,7 @@ import { OutputEntry } from '../../interfaces';
import { AppState } from '../state';
import { WrapperMosaicId } from './output-editors-wrapper';
export interface CommandsProps {
appState: AppState;
// Used to keep testing conform
@@ -18,10 +19,10 @@ export interface CommandsProps {
* whenever a Fiddle is launched in Electron.
*
* @class Output
* @extends {React.Component<CommandsProps>}
* @extends {React.Component<CommandsProps, {}>}
*/
@observer
export class Output extends React.Component<CommandsProps> {
export class Output extends React.Component<CommandsProps, {}> {
public static contextType = MosaicContext;
public context: MosaicContext<WrapperMosaicId>;
private outputRef = React.createRef<HTMLDivElement>();
@@ -33,6 +34,7 @@ export class Output extends React.Component<CommandsProps> {
this.renderEntry = this.renderEntry.bind(this);
}
public componentDidMount() {
autorun(() => {
const { isConsoleShowing } = this.props.appState;
@@ -81,16 +83,13 @@ export class Output extends React.Component<CommandsProps> {
*/
public renderEntry(entry: OutputEntry, index: number): Array<JSX.Element> {
const ts = this.renderTimestamp(entry.timestamp);
const timestamp = <span className="timestamp">{ts}</span>;
const timestamp = <span className='timestamp'>{ts}</span>;
const lines = entry.text.split(/\r?\n/);
const style: React.CSSProperties = entry.isNotPre
? { whiteSpace: 'initial' }
: {};
const style: React.CSSProperties = entry.isNotPre ? { whiteSpace: 'initial' } : {};
return lines.map((text, lineIndex) => (
<p style={style} key={`${entry.timestamp}--${index}--${lineIndex}`}>
{timestamp}
{text}
{timestamp}{text}
</p>
));
}
@@ -108,7 +107,7 @@ export class Output extends React.Component<CommandsProps> {
.map(this.renderEntry);
return (
<div className="output" ref={this.outputRef}>
<div className='output' ref={this.outputRef}>
{lines}
</div>
);
+17 -24
View File
@@ -30,15 +30,12 @@ export interface Contributor {
* @class CreditsSettings
* @extends {React.Component<CreditsSettingsProps, CreditsSettingsState>}
*/
export class CreditsSettings extends React.Component<
CreditsSettingsProps,
CreditsSettingsState
> {
export class CreditsSettings extends React.Component<CreditsSettingsProps, CreditsSettingsState> {
constructor(props: CreditsSettingsProps) {
super(props);
this.state = {
contributors: [],
contributors: []
};
this.getContributors();
@@ -53,25 +50,22 @@ export class CreditsSettings extends React.Component<
const { contributors } = this.state;
return contributors.map(({ name, avatar, url, login, location, bio }) => {
const maybeLocation = location ? (
<p className="location">📍 {location}</p>
) : null;
const maybeBio = bio ? <small className="bio">{bio}</small> : null;
const maybeLocation = location
? <p className='location'>📍 {location}</p>
: null;
const maybeBio = bio
? <small className='bio'>{bio}</small>
: null;
const style: React.CSSProperties = {
backgroundImage: `url(${avatar})`,
backgroundImage: `url(${avatar})`
};
const onClick = () => shell.openExternal(url);
return (
<Card
interactive={true}
key={login}
className="contributor"
onClick={onClick}
>
<div className="avatar" style={style} />
<div className="details">
<h5 className="name">{name || login}</h5>
<Card interactive={true} key={login} className='contributor' onClick={onClick}>
<div className='avatar' style={style} />
<div className='details'>
<h5 className='name'>{name || login}</h5>
{maybeLocation}
{maybeBio}
</div>
@@ -90,17 +84,16 @@ export class CreditsSettings extends React.Component<
would like to thank those who helped to make Electron Fiddle:
</Callout>
<br />
<div className="contributors">{this.renderContributors()}</div>
<div className='contributors'>
{this.renderContributors()}
</div>
</div>
);
}
public async getContributors() {
try {
const contributorsFile = path.join(
__dirname,
'../../static/contributors.json',
);
const contributorsFile = path.join(__dirname, '../../static/contributors.json');
const contributors = await fs.readJSON(contributorsFile);
this.setState({ contributors });
} catch (error) {
+87 -83
View File
@@ -7,15 +7,14 @@ import {
HTMLTable,
IButtonProps,
Icon,
IconName,
Tooltip,
IconName
} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { RunnableVersion, VersionSource, VersionState } from '../../interfaces';
import { ElectronVersion, ElectronVersionSource, VersionState } from '../../interfaces';
import { normalizeVersion } from '../../utils/normalize-version';
import { sortedElectronMap } from '../../utils/sorted-electron-map';
import { sortedElectronMap } from '../../utils/sorted-map';
import { AppState } from '../state';
import { ElectronReleaseChannel, getReleaseChannel } from '../versions';
@@ -35,10 +34,7 @@ export interface ElectronSettingsState {
* @extends {React.Component<ElectronSettingsProps, {}>}
*/
@observer
export class ElectronSettings extends React.Component<
ElectronSettingsProps,
ElectronSettingsState
> {
export class ElectronSettings extends React.Component<ElectronSettingsProps, ElectronSettingsState> {
constructor(props: ElectronSettingsProps) {
super(props);
@@ -51,7 +47,7 @@ export class ElectronSettings extends React.Component<
this.state = {
isDownloadingAll: false,
isDeletingAll: false,
isDeletingAll: false
};
}
@@ -64,7 +60,9 @@ export class ElectronSettings extends React.Component<
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleStateChange(event: React.FormEvent<HTMLInputElement>) {
public handleStateChange(
event: React.FormEvent<HTMLInputElement>
) {
const { id, checked } = event.currentTarget;
const { appState } = this.props;
@@ -80,14 +78,16 @@ export class ElectronSettings extends React.Component<
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleChannelChange(event: React.FormEvent<HTMLInputElement>) {
public handleChannelChange(
event: React.FormEvent<HTMLInputElement>
) {
const { id, checked } = event.currentTarget;
const { appState } = this.props;
if (!checked) {
appState.channelsToShow = appState.channelsToShow.filter((c) => c !== id);
appState.versionsToShow = appState.versionsToShow.filter((c) => c !== id);
} else {
appState.channelsToShow.push(id as ElectronReleaseChannel);
appState.versionsToShow.push(id as ElectronReleaseChannel);
}
}
@@ -137,7 +137,7 @@ export class ElectronSettings extends React.Component<
public render() {
return (
<div className="settings-electron">
<div className='settings-electron'>
<h2>Electron Settings</h2>
<Callout>
{this.renderVersionChannelOptions()}
@@ -169,25 +169,25 @@ export class ElectronSettings extends React.Component<
disabled={isUpdatingElectronVersions}
onClick={this.handleDownloadClick}
loading={isUpdatingElectronVersions}
icon="numbered-list"
text="Update Electron Release List"
icon='numbered-list'
text='Update Electron Release List'
/>
<Button
disabled={isWorking}
icon="download"
icon='download'
onClick={this.handleDownloadAll}
text="Download All Versions"
text='Download All Versions'
/>
<Button
disabled={isWorking}
icon="trash"
icon='trash'
onClick={this.handleDeleteAll}
text="Delete All Downloads"
text='Delete All Downloads'
/>
<Button
icon="document-open"
icon='document-open'
onClick={this.handleAddVersion}
text="Add Local Electron Build"
text='Add Local Electron Build'
/>
</ButtonGroup>
);
@@ -207,28 +207,27 @@ export class ElectronSettings extends React.Component<
};
return (
<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>
<FormGroup
label='Include Electron versions that are:'
>
<Checkbox
checked={getIsChecked(VersionState.downloading)}
label="Downloading"
id="downloading"
label='Downloading'
id='downloading'
onChange={this.handleStateChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(VersionState.ready)}
label='Downloaded'
id='ready'
onChange={this.handleStateChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(VersionState.unknown)}
label="Not Downloaded"
id="unknown"
label='Not Downloaded'
id='unknown'
onChange={this.handleStateChange}
inline={true}
/>
@@ -244,44 +243,42 @@ export class ElectronSettings extends React.Component<
*/
private renderVersionChannelOptions(): JSX.Element {
const { appState } = this.props;
const getIsChecked = (channel: ElectronReleaseChannel) => {
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 appState.versionsToShow.includes(channel);
};
return (
<FormGroup label="Include Electron versions from these release channels:">
{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
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}
/>
</FormGroup>
);
}
@@ -294,15 +291,20 @@ export class ElectronSettings extends React.Component<
*/
private renderTable(): JSX.Element {
return (
<HTMLTable className="electron-versions-table" striped={true}>
<HTMLTable
className='electron-versions-table'
striped={true}
>
<thead>
<tr>
<th>Version</th>
<th>Status</th>
<th className="action">Action</th>
<th className='action'>Action</th>
</tr>
</thead>
<tbody>{this.renderTableRows()}</tbody>
<tbody>
{this.renderTableRows()}
</tbody>
</HTMLTable>
);
}
@@ -314,11 +316,11 @@ export class ElectronSettings extends React.Component<
* @returns {Array<JSX.Element>}
*/
private renderTableRows(): Array<JSX.Element | null> {
const { versions, channelsToShow, statesToShow } = this.props.appState;
const { versions, versionsToShow, statesToShow } = this.props.appState;
return sortedElectronMap<JSX.Element | null>(versions, (key, item) => {
// Check if we want to show the version
if (!channelsToShow.includes(getReleaseChannel(item))) {
if (!versionsToShow.includes(getReleaseChannel(item))) {
return null;
}
@@ -331,7 +333,7 @@ export class ElectronSettings extends React.Component<
<tr key={item.version}>
<td>{item.version}</td>
<td>{this.renderHumanState(item)}</td>
<td className="action">{this.renderAction(key, item)}</td>
<td className='action'>{this.renderAction(key, item)}</td>
</tr>
);
});
@@ -340,10 +342,10 @@ export class ElectronSettings extends React.Component<
/**
* Returns a human-readable state indicator for an Electron version
*
* @param {RunnableVersion} item
* @param {ElectronVersion} item
* @returns {JSX.Element}
*/
private renderHumanState(item: RunnableVersion): JSX.Element {
private renderHumanState(item: ElectronVersion): JSX.Element {
const { state } = item;
let icon: IconName = 'box';
let humanState = 'Downloaded';
@@ -368,22 +370,24 @@ export class ElectronSettings extends React.Component<
*
* @private
* @param {string} key
* @param {RunnableVersion} item
* @param {ElectronVersion} item
* @returns {JSX.Element}
*/
private renderAction(key: string, item: RunnableVersion): JSX.Element {
private renderAction(key: string, item: ElectronVersion): JSX.Element {
const { state, source } = item;
const { appState } = this.props;
const buttonProps: IButtonProps = {
fill: true,
small: true,
small: true
};
// Already downloaded
if (state === 'ready') {
buttonProps.onClick = () => appState.removeVersion(key);
buttonProps.icon = 'trash';
buttonProps.text = source === VersionSource.local ? 'Remove' : 'Delete';
buttonProps.text = source === ElectronVersionSource.local
? 'Remove'
: 'Delete';
} else if (state === 'downloading') {
buttonProps.disabled = true;
buttonProps.loading = true;
+13 -90
View File
@@ -1,15 +1,7 @@
import {
Callout,
Checkbox,
FormGroup,
InputGroup,
Radio,
RadioGroup,
} from '@blueprintjs/core';
import { Callout, Checkbox, FormGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { IPackageManager } from '../npm';
import { AppState } from '../state';
export interface ExecutionSettingsProps {
@@ -23,15 +15,12 @@ export interface ExecutionSettingsProps {
* @extends {React.Component<ExecutionSettingsProps, {}>}
*/
@observer
export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {}> {
constructor(props: ExecutionSettingsProps) {
super(props);
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(
this,
);
this.handleExecutionFlagChange = this.handleExecutionFlagChange.bind(this);
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(this);
}
/**
@@ -40,7 +29,9 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleDeleteDataChange(event: React.FormEvent<HTMLInputElement>) {
public handleDeleteDataChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
this.props.appState.isKeepingUserDataDirs = checked;
}
@@ -50,28 +41,15 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleElectronLoggingChange(event: React.FormEvent<HTMLInputElement>) {
public handleElectronLoggingChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
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,
executionFlags = [],
} = this.props.appState;
const { isKeepingUserDataDirs, isEnablingElectronLogging } = this.props.appState;
const deleteUserDirLabel = `
Whenever Electron runs, it creates a user data directory for cookies, the cache,
@@ -87,15 +65,14 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
<div>
<h2>Execution</h2>
<Callout>
These advanced settings control how Electron Fiddle executes your
fiddles.
These advanced settings control how Electron Fiddle executes your fiddles.
</Callout>
<br />
<Callout>
<FormGroup label={deleteUserDirLabel}>
<Checkbox
checked={isKeepingUserDataDirs}
label="Do not delete user data directories."
label='Do not delete user data directories.'
onChange={this.handleDeleteDataChange}
/>
</FormGroup>
@@ -105,66 +82,12 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
<FormGroup label={electronLoggingLabel}>
<Checkbox
checked={isEnablingElectronLogging}
label="Enable advanced Electron logging."
label='Enable advanced Electron logging.'
onChange={this.handleElectronLoggingChange}
/>
</FormGroup>
</Callout>
<br />
<Callout>
<FormGroup>
<p>
Electron allows starting the executable with{' '}
<a href="https://www.electronjs.org/docs/api/command-line-switches">
user-provided flags
</a>
, such as &apos;--js-flags=--expose-gc&apos;. 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>
<br />
<Callout>
<FormGroup>
<span style={{ marginRight: 4 }}>
Electron Fiddle will install packages on runtime if they are
imported within your fiddle with <code>require</code>. It uses{' '}
<a href="https://www.npmjs.com/" target="_blank" rel="noreferrer">
npm
</a>{' '}
as its package manager by default, but{' '}
<a
href="https://classic.yarnpkg.com/lang/en/"
target="_blank"
rel="noreferrer"
>
Yarn
</a>{' '}
is also available.
</span>
<RadioGroup
onChange={this.handlePMChange}
selectedValue={this.props.appState.packageManager}
inline={true}
>
<Radio label="npm" value="npm" />
<Radio label="yarn" value="yarn" />
</RadioGroup>
</FormGroup>
</Callout>
</div>
);
}
private handlePMChange = (event: React.FormEvent<HTMLInputElement>) => {
this.props.appState.packageManager = event.currentTarget
.value as IPackageManager;
};
}
@@ -19,28 +19,23 @@ const ThemeSelect = Select.ofType<LoadedFiddleTheme>();
* version.
*
* @param {string} query
* @param {RunnableVersion} { version }
* @param {ElectronVersion} { version }
* @returns
*/
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (
query,
{ name },
) => {
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
return name.toLowerCase().includes(query.toLowerCase());
};
/**
* Helper method: Returns the <Select /> <MenuItem /> for Electron
* versions.
*
* @param {RunnableVersion} item
* @param {ElectronVersion} item
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
export const renderItem: ItemRenderer<LoadedFiddleTheme> = (
item,
{ handleClick, modifiers, query },
) => {
export const renderItem: ItemRenderer<LoadedFiddleTheme> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
@@ -52,7 +47,7 @@ export const renderItem: ItemRenderer<LoadedFiddleTheme> = (
text={highlightText(item.name, query)}
key={item.name}
onClick={handleClick}
icon="media"
icon='media'
/>
);
};
@@ -74,8 +69,7 @@ export interface AppearanceSettingsState {
*/
@observer
export class AppearanceSettings extends React.Component<
AppearanceSettingsProps,
AppearanceSettingsState
AppearanceSettingsProps, AppearanceSettingsState
> {
public constructor(props: AppearanceSettingsProps) {
super(props);
@@ -85,13 +79,13 @@ export class AppearanceSettings extends React.Component<
this.handleAddTheme = this.handleAddTheme.bind(this);
this.state = {
themes: [],
themes: []
};
getAvailableThemes().then((themes) => {
const { theme } = this.props.appState;
const selectedTheme =
(theme && themes.find(({ file }) => file === theme)) || themes[0];
const selectedTheme = theme &&
themes.find(({ file }) => file === theme) || themes[0];
this.setState({ themes, selectedTheme });
});
@@ -127,20 +121,16 @@ export class AppearanceSettings extends React.Component<
const name = namor.generate({ words: 2, numbers: 0 });
const themePath = path.join(THEMES_PATH, `${name}.json`);
await fs.outputJSON(
themePath,
{
...theme,
name,
file: undefined,
css: undefined,
},
{ spaces: 2 },
);
await fs.outputJSON(themePath, {
...theme,
name,
file: undefined,
css: undefined
}, { spaces: 2 });
shell.showItemInFolder(themePath);
this.setState({ themes: await getAvailableThemes() });
this.setState({themes: await getAvailableThemes()});
return true;
} catch (error) {
@@ -171,46 +161,51 @@ export class AppearanceSettings extends React.Component<
public render() {
const { selectedTheme } = this.state;
const selectedName =
(selectedTheme && selectedTheme.name) || 'Select a theme';
const selectedName = selectedTheme && selectedTheme.name || 'Select a theme';
return (
<div className="settings-appearance">
<div className='settings-appearance'>
<h4>Appearance</h4>
<FormGroup label="Choose your theme" inline={true}>
<FormGroup
label='Choose your theme'
inline={true}
>
<ThemeSelect
filterable={true}
items={this.state.themes}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.handleChange}
noResults={<MenuItem disabled={true} text="No results." />}
noResults={<MenuItem disabled={true} text='No results.' />}
>
<Button text={selectedName} icon="tint" />
<Button
text={selectedName}
icon='tint'
/>
</ThemeSelect>
</FormGroup>
<Callout>
<p>
To add themes, add JSON theme files to{' '}
<a id="open-theme-folder" onClick={this.openThemeFolder}>
To add themes, add JSON theme files to <a
id='open-theme-folder'
onClick={this.openThemeFolder}
>
<code>{THEMES_PATH}</code>
</a>
. The easiest way to get started is to clone one of the two existing
</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.
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"
text='Create theme from current selection'
icon='duplicate'
/>
<Button
icon="document-open"
icon='document-open'
onClick={this.handleAddTheme}
text="Add a Monaco Editor theme"
text='Add a Monaco Editor theme'
/>
</Callout>
</div>
@@ -223,4 +218,5 @@ export class AppearanceSettings extends React.Component<
public handleAddTheme(): void {
this.props.appState.toggleAddMonacoThemeDialog();
}
}
@@ -12,10 +12,10 @@ export interface ConsoleSettingsProps {
* Settings content to manage console-related preferences.
*
* @class ConsoleSettings
* @extends {React.Component<ConsoleSettingsProps>}
* @extends {React.Component<ConsoleSettingsProps, {}>}
*/
@observer
export class ConsoleSettings extends React.Component<ConsoleSettingsProps> {
export class ConsoleSettings extends React.Component<ConsoleSettingsProps, {}> {
constructor(props: ConsoleSettingsProps) {
super(props);
@@ -28,7 +28,9 @@ export class ConsoleSettings extends React.Component<ConsoleSettingsProps> {
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleClearOnRunChange(event: React.FormEvent<HTMLInputElement>) {
public handleClearOnRunChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
this.props.appState.isClearingConsoleOnRun = checked;
}
@@ -47,7 +49,7 @@ export class ConsoleSettings extends React.Component<ConsoleSettingsProps> {
<FormGroup label={clearOnRunLabel}>
<Checkbox
checked={isClearingConsoleOnRun}
label="Clear on run."
label='Clear on run.'
onChange={this.handleClearOnRunChange}
/>
</FormGroup>
@@ -15,7 +15,7 @@ export interface GitHubSettingsProps {
* @extends {React.Component<GitHubSettingsProps, {}>}
*/
@observer
export class GitHubSettings extends React.Component<GitHubSettingsProps> {
export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
constructor(props: GitHubSettingsProps) {
super(props);
@@ -31,10 +31,10 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps> {
return (
<Callout>
<p>
Your fiddles can be published as GitHub Gists - that way you can share
your fiddles with the world!
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" />
<Button onClick={this.signIn} icon='log-in' text='Sign in'/>
</Callout>
);
}
@@ -51,11 +51,11 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps> {
return (
<Callout>
<p>
Your fiddles can be published as public GitHub Gists. Using the
personal access token you gave us, we logged you into GitHub as{' '}
<code>{gitHubLogin}</code>.
Your fiddles can be published as public GitHub Gists.
Using the personal access token you gave us, we
logged you into GitHub as <code>{gitHubLogin}</code>.
</p>
<Button onClick={signOut} icon="log-out" text="Sign out" />
<Button onClick={signOut} icon='log-out' text='Sign out'/>
</Callout>
);
}
+2 -2
View File
@@ -15,10 +15,10 @@ export interface GeneralSettingsProps {
* Settings content to manage GitHub-related preferences.
*
* @class GitHubSettings
* @extends {React.Component<GeneralSettingsProps>}
* @extends {React.Component<GeneralSettingsProps, {}>}
*/
@observer
export class GeneralSettings extends React.Component<GeneralSettingsProps> {
export class GeneralSettings extends React.Component<GeneralSettingsProps, {}> {
public render() {
return (
<div>
+8 -8
View File
@@ -12,14 +12,14 @@ enum SettingsSections {
General = 'General',
Electron = 'Electron',
Execution = 'Execution',
Credits = 'Credits',
Credits = 'Credits'
}
const settingsSections = [
SettingsSections.General,
SettingsSections.Electron,
SettingsSections.Execution,
SettingsSections.Credits,
SettingsSections.Credits
];
export interface SettingsProps {
@@ -42,7 +42,7 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
super(props);
this.state = {
section: SettingsSections.General,
section: SettingsSections.General
};
this.closeSettingsPanel = this.closeSettingsPanel.bind(this);
@@ -117,13 +117,13 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
if (!isSettingsShowing) return null;
return (
<div className="settings">
<div className="settings-menu">
<div className='settings'>
<div className='settings-menu'>
<ul>{this.renderOptions()}</ul>
</div>
<div className="settings-content">
<div className="settings-close" onClick={appState.toggleSettings}>
<Icon icon="cross" />
<div className='settings-content'>
<div className='settings-close' onClick={appState.toggleSettings}>
<Icon icon='cross' />
</div>
{this.renderContent()}
</div>
+4 -5
View File
@@ -13,17 +13,16 @@ export interface ShowMeProps {
* panel.
*
* @class ShowMe
* @extends {React.Component<ShowMeProps>}
* @extends {React.Component<ShowMeProps, ShowMeState>}
*/
@observer
export class ShowMe extends React.Component<ShowMeProps> {
export class ShowMe extends React.Component<ShowMeProps, {}> {
public render() {
const { currentDocsDemoPage: showMeName } = this.props.appState;
const Content =
DOCS_DEMO_COMPONENTS[showMeName] || DOCS_DEMO_COMPONENTS.DEFAULT;
const Content = DOCS_DEMO_COMPONENTS[showMeName] || DOCS_DEMO_COMPONENTS.DEFAULT;
return (
<div className="show-me-panel">
<div className='show-me-panel'>
<Content appState={this.props.appState} />
</div>
);
+48 -47
View File
@@ -24,21 +24,18 @@ function ShowHide() {
return (
<>
<p>
This button hides Electron Fiddle right away, showing it again in two
seconds.
</p>
<p>This button hides Electron Fiddle right away, showing it again in two seconds.</p>
<Button
id="show-hide"
icon="eye-off"
text="Hide Electron Fiddle"
id='show-hide'
icon='eye-off'
text='Hide Electron Fiddle'
onClick={onClick}
/>
</>
);
}
export function ShowMeApp(): JSX.Element {
const [secondsLeft, setSeconds] = React.useState(-1);
export function ShowMeApp(_props: any): JSX.Element {
const [ secondsLeft, setSeconds ] = React.useState(-1);
const playFocus = () => {
setSeconds(3);
@@ -50,7 +47,7 @@ export function ShowMeApp(): JSX.Element {
}, 3000);
};
const [paths, setPaths] = React.useState('');
const [ paths, setPaths ] = React.useState('');
const playPaths = () => {
const pathsToQuery = [
'home',
@@ -58,7 +55,7 @@ export function ShowMeApp(): JSX.Element {
'userData',
'temp',
'downloads',
'desktop',
'desktop'
];
let result = '';
@@ -69,71 +66,75 @@ export function ShowMeApp(): JSX.Element {
setPaths(result);
};
const [metrics, setMetrics] = React.useState('');
const [ metrics, setMetrics ] = React.useState('');
const playMetrics = () => {
setMetrics(JSON.stringify(remote.app.getAppMetrics(), undefined, 2));
};
return (
<>
<Icon
icon="help"
iconSize={40}
style={{ float: 'left', margin: '0 10px 0 0' }}
/>
<p className="bp3r-running-text">
The <code>app</code> module controls the app&apos;s application
life-cycle. Most of the events and methods available on this module are
responsible for handling how your interacts with the operating system or
to set application-wide settings.
<Icon icon='help' iconSize={40} style={{ float: 'left', margin: '0 10px 0 0' }} />
<p className='bp3r-running-text'>
The <code>app</code> module controls the app's application life-cycle. Most of the
events and methods available on this module are responsible for handling how your
interacts with the operating system or to set application-wide settings.
</p>
<h3>API Demos</h3>
{getSubsetOnly('app')}
<Callout title="Hiding, Showing, Focussing" icon="eye-open">
<Callout
title='Hiding, Showing, Focussing'
icon='eye-open'
>
<p>
The app can ask the operating system for window focus. On macOS, it
can additionally request that the app be hidden or shown. Give it a
try: Click on the button below, focus another app, and wait for two
seconds to see Electron Fiddle become the focused app again.
The app can ask the operating system for window focus. On macOS, it can additionally
request that the app be hidden or shown. Give it a try: Click on the button below,
focus another app, and wait for two seconds to see Electron Fiddle become the focused
app again.
</p>
<Button
id="focus"
icon="lightbulb"
text={`Focus Electron Fiddle${
secondsLeft > 0 ? ` in ${secondsLeft}s` : ''
}`}
id='focus'
icon='lightbulb'
text={`Focus Electron Fiddle${secondsLeft > 0 ? ` in ${secondsLeft}s` : ''}`}
onClick={playFocus}
/>
<ShowHide />
</Callout>
<Callout title="Paths" icon="folder-open">
<Callout
title='Paths'
icon='folder-open'
>
<p>
Need to query information about various paths in a cross-platform
manner? Electron can help. The button queries the operating system for
some of them.
Need to query information about various paths in a cross-platform manner? Electron
can help. The button queries the operating system for some of them.
</p>
<Button
id="special-paths"
icon="play"
id='special-paths'
icon='play'
text={`Get special directory paths`}
onClick={playPaths}
/>
<pre id="special-paths-content">{paths}</pre>
<pre id='special-paths-content'>
{paths}
</pre>
</Callout>
<Callout title="Process & Device Information" icon="pulse">
<Callout
title='Process & Device Information'
icon='pulse'
>
<p>
Need to query information about the process or system? The{' '}
<code>app</code> module lets developers query for information about
the running app, hardware, and operating system. A good example are
process metrics.
Need to query information about the process or system? The <code>app</code> module lets
developers query for information about the running app, hardware, and operating system.
A good example are process metrics.
</p>
<Button
id="process-metrics"
icon="play"
id='process-metrics'
icon='play'
text={`Get process metrics`}
onClick={playMetrics}
/>
<pre id="process-metrics-content">{metrics}</pre>
<pre id='process-metrics-content'>
{metrics}
</pre>
</Callout>
{renderMoreDocumentation()}
</>
+11 -15
View File
@@ -14,33 +14,29 @@ export function ShowMeDefault(props: { appState: AppState }): JSX.Element {
<li key={key}>
<Button
minimal={true}
icon="play"
icon='play'
text={DOCS_DEMO_NAMES[key]}
onClick={() =>
(props.appState.currentDocsDemoPage = key as DocsDemoPage)
}
onClick={() => (props.appState.currentDocsDemoPage = key as DocsDemoPage)}
/>
</li>
);
});
});
return (
<>
<Icon
icon="help"
iconSize={40}
style={{ float: 'left', margin: '0 10px 0 0' }}
/>
<p className="bp3r-running-text">
This panel offers useful information about Electron APIs  and easy way
to try some of the methods it offers. Fiddle has an example fiddle for
every module available in Electron.
<Icon icon='help' iconSize={40} style={{ float: 'left', margin: '0 10px 0 0' }} />
<p className='bp3r-running-text'>
This panel offers useful information about Electron APIs  and
easy way to try some of the methods it offers. Fiddle has an example fiddle
for every module available in Electron.
</p>
<p>
Clicking on any of the modules below will open up a fiddle showcasing
that particular module.
</p>
<ul className="show-me-list">{...showMeMenu}</ul>
<ul className='show-me-list'>
{...showMeMenu}
</ul>
{renderMoreDocumentation()}
</>
);
+2 -2
View File
@@ -5,10 +5,10 @@ import { ShowMeDefault } from './default';
export const DOCS_DEMO_COMPONENTS: Record<DocsDemoPage, any> = {
DEFAULT: ShowMeDefault,
DEMO_APP: ShowMeApp,
DEMO_APP: ShowMeApp
};
export const DOCS_DEMO_NAMES: Record<DocsDemoPage, string> = {
DEFAULT: 'Home',
DEMO_APP: 'App Demos',
DEMO_APP: 'App Demos'
};
@@ -10,7 +10,7 @@ import { shell } from 'electron';
* @returns {JSX.Element}
*/
export function renderMoreDocumentation(
url = 'electronjs.org/docs',
url: string = 'electronjs.org/docs'
): JSX.Element {
const fullUrl = `https://${url}`;
const gitHubUrl = `https://github.com/electron/fiddle`;
@@ -18,21 +18,14 @@ export function renderMoreDocumentation(
return (
<>
<br />
<p className="b3-running-text">
For more documentation, visit{' '}
<a id="open-url" onClick={() => shell.openExternal(fullUrl)}>
{url}
</a>
, where you can find the full documentation for Electron.
<p className='b3-running-text'>
For more documentation, visit <a id='open-url' onClick={() => shell.openExternal(fullUrl)}>{url}</a>, where
you can find the full documentation for Electron.
</p>
<p className="bp3-text-muted">
By the way, Electron Fiddle and the documentation you see here is
entirely open source. If you have ideas on how to improve it, we&apos;d
love to have your contributions! You can find the repository on{' '}
<a id="open-github" onClick={() => shell.openExternal(gitHubUrl)}>
GitHub
</a>
.
<p className='bp3-text-muted'>
By the way, Electron Fiddle and the documentation you see here is entirely open source. If you
have ideas on how to improve it, we'd love to have your contributions! You can find the
repository on <a id='open-github' onClick={() => shell.openExternal(gitHubUrl)}>GitHub</a>.
</p>
</>
);
@@ -17,15 +17,10 @@ export function getSubsetOnly(moduleName: string): JSX.Element {
const { full, short } = getDocsUrlForModule(moduleName);
return (
<p className="bp3-running-text">
The following demos display only a subset of what the{' '}
<code>{moduleName}</code>
<p className='bp3-running-text'>
The following demos display only a subset of what the <code>{moduleName}</code>
module is capable of. If you want to see its full abilities, check out the
documentation on{' '}
<a id="open-url" onClick={() => shell.openExternal(full)}>
{short}
</a>
.
documentation on <a id='open-url' onClick={() => shell.openExternal(full)}>{short}</a>.
</p>
);
}
+74 -76
View File
@@ -28,17 +28,17 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
Electron Fiddle allows you to build little experiments and mini-apps
with Electron. Each Fiddle has at least three of these files: A main
script, a renderer script, a preload script, and an HTML file.
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.
</p>
<p>
If you <code>require()</code> a module, Fiddle will install it
automatically. It will also automatically provide you with
autocomplete information for the <code>electron</code> module.
If you <code>require()</code> a module, Fiddle will install
it automatically. It will also automatically provide you with autocomplete
information for the <code>electron</code> module.
</p>
</>
),
)
},
{
name: 'select-versions',
@@ -47,21 +47,25 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
Electron Fiddle knows about all released Electron versions,
downloading your versions automatically in the background.
Electron Fiddle knows about all released Electron versions, downloading
your versions automatically in the background.
</p>
<p>
Open the preferences to see all available versions and delete those
previously downloaded.
Open the preferences to see all available versions and delete those previously
downloaded.
</p>
</>
),
)
},
{
name: 'button-run',
selector: '.button-run',
title: '🚀 Run Your Fiddle',
content: <p>Hit this button to give your Fiddle a try and start it.</p>,
content: (
<p>
Hit this button to give your Fiddle a try and start it.
</p>
)
},
{
name: 'button-publish',
@@ -70,17 +74,17 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
Like what you&apos;ve built? You can save your Fiddle as a public
GitHub Gist, allowing other users to load it by pasting the URL into
the address bar. If they don&apos;t have Electron Fiddle, they can
see and download your code directly from GitHub.
Like what you've built? You can save your Fiddle as a public GitHub Gist,
allowing other users to load it by pasting the URL into the address bar.
If they don't have Electron Fiddle, they can see and download your code
directly from GitHub.
</p>
<p>
You can also package your Fiddle as a standalone binary or as an
installer from the &quot;Tasks&quot; menu.
You can also package your Fiddle as a standalone binary or as an installer
from the "Tasks" menu.
</p>
</>
),
)
},
{
name: 'first-time-electron',
@@ -88,25 +92,17 @@ export function getWelcomeTour(): Set<TourScriptStep> {
title: '👋 Getting Started With Electron?',
content: (
<p>
We&apos;ve finished our tour of Electron Fiddle, but if this is your
We've finished our tour of Electron Fiddle, but if this is your
first time using Electron, we could introduce you to its basics.
Interested?
</p>
),
getButtons: ({
stop,
advance,
}: TourStepGetButtonParams): Array<JSX.Element> => {
getButtons: ({ stop, advance }: TourStepGetButtonParams): Array<JSX.Element> => {
return [
<Button key="btn-stop" onClick={stop} text="I'm good!" icon="stop" />,
<Button
key="btn-adv"
onClick={advance}
text="Electron Basics"
icon="help"
/>,
<Button key='btn-stop' onClick={stop} text="I'm good!" icon='stop' />,
<Button key='btn-adv' onClick={advance} text='Electron Basics' icon='help' />
];
},
}
},
{
name: 'main-editor',
@@ -115,24 +111,24 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
Every Electron app starts with a main script, very similar to how a
Node.js application is started. The main script runs in the
&quot;main process&quot;. To display a user interface, the main
process creates renderer processes  usually in the form of windows,
which Electron calls &nbsp;<code>BrowserWindow</code>.
Every Electron app starts with a main script, very similar to how
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
&nbsp;<code>BrowserWindow</code>.
</p>
<p>
To get started, pretend that the main process is just like a Node.js
process. All APIs and features found in Electron are accessible
through the <code>electron</code> module, which can be required like
any other Node.js module.
process. All APIs and features found in Electron are accessible through
the <code>electron</code> module, which can be required like any other
Node.js module.
</p>
<p>
The default fiddle creates a new <code>BrowserWindow</code> and
loads an HTML file.
The default fiddle creates a new <code>BrowserWindow</code> and loads
an HTML file.
</p>
</>
),
)
},
{
name: 'html-editor',
@@ -140,14 +136,14 @@ export function getWelcomeTour(): Set<TourScriptStep> {
title: '📝 HTML',
content: (
<p>
In the default fiddle, this HTML file is loaded in the &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 &nbsp;
<code>&lt;script /&gt;</code> tag and notice how we can call{' '}
<code>require()</code> like we would in Node.js.
In the default fiddle, this HTML file is loaded in the
&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
&nbsp;<code>&lt;script /&gt;</code> tag and notice how we can call <code>
require()</code> like we would in Node.js.
</p>
),
)
},
{
name: 'renderer-editor',
@@ -156,19 +152,17 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<>
<p>
This is the script we just required from the HTML file. In here, you
can do anything that works in Node.js <i>and</i> anything that works
in a browser.
This is the script we just required from the HTML file. In here, you can
do anything that works in Node.js <i>and</i> anything that works in a browser.
</p>
<p>
By the way: If you want to use an <code>npm</code> module here, just
&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.
&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>
</>
),
},
)
}
]);
}
@@ -180,10 +174,7 @@ export function getWelcomeTour(): Set<TourScriptStep> {
* @extends {React.Component<WelcomeTourProps, WelcomeTourState>}
*/
@observer
export class WelcomeTour extends React.Component<
WelcomeTourProps,
WelcomeTourState
> {
export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourState> {
constructor(props: WelcomeTourProps) {
super(props);
@@ -191,7 +182,7 @@ export class WelcomeTour extends React.Component<
this.startTour = this.startTour.bind(this);
this.state = {
isTourStarted: false,
isTourStarted: false
};
}
@@ -213,16 +204,16 @@ export class WelcomeTour extends React.Component<
return (
<>
<Button
key="cancel"
key='cancel'
onClick={this.stopTour}
icon="cross"
icon='cross'
text={`I'll figure it out`}
/>
<Button
key="ok"
key='ok'
onClick={this.startTour}
icon="presentation"
text="Show me around"
icon='presentation'
text='Show me around'
/>
</>
);
@@ -236,27 +227,34 @@ export class WelcomeTour extends React.Component<
if (!isTourStarted) {
return (
<Dialog key="welcome-tour-dialog" isOpen={true}>
<Dialog
key='welcome-tour-dialog'
isOpen={true}
>
<div className={Classes.DIALOG_HEADER}>
<h4 className={Classes.HEADING}>🙋 Hey There!</h4>
</div>
<div className={Classes.DIALOG_BODY}>
<p>
Welcome to Electron Fiddle! If you&apos;re new to the app,
we&apos;d like to give you a brief tour of its features.
Welcome to Electron Fiddle! If you're new to the app,
we'd like to give you a brief tour of its features.
</p>
<p>
We won&apos;t show this dialog again, but you can always find the
tour in the Help menu.
We won't show this dialog again, but you can always
find the tour in the Help menu.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>{this.buttons}</div>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
{this.buttons}
</div>
</div>
</Dialog>
);
} else {
return <Tour tour={getWelcomeTour()} onStop={this.stopTour} />;
return (
<Tour tour={getWelcomeTour()} onStop={this.stopTour} />
);
}
}
}
+48 -57
View File
@@ -40,7 +40,7 @@ export class Tour extends React.Component<TourProps, TourState> {
this.state = {
tour: props.tour.entries(),
step: null,
i: 0,
i: 0
};
}
@@ -92,7 +92,11 @@ export class Tour extends React.Component<TourProps, TourState> {
if (!step) return null;
return <div className="tour">{this.getStep(step)}</div>;
return (
<div className='tour'>
{this.getStep(step)}
</div>
);
}
/**
@@ -108,34 +112,36 @@ export class Tour extends React.Component<TourProps, TourState> {
if (getButtons) {
return getButtons({
stop: this.stop,
advance: this.advance,
advance: this.advance
});
}
// No? Fine! Are we at the end of the tour?
return this.props.tour.size === this.state.i
? [
? [(
<Button
icon='tick-circle'
onClick={this.stop}
key='btn-stop'
text='Finish Tour'
/>
)] : [
(
<Button
icon="tick-circle"
icon='stop'
key='btn-stop'
onClick={this.stop}
key="btn-stop"
text="Finish Tour"
/>,
]
: [
text='Stop Tour'
/>
), (
<Button
icon="stop"
key="btn-stop"
onClick={this.stop}
text="Stop Tour"
/>,
<Button
icon="step-forward"
key="btn-adv"
icon='step-forward'
key='btn-adv'
onClick={this.advance}
text="Continue"
/>,
];
text='Continue'
/>
)
];
}
/**
@@ -145,10 +151,7 @@ export class Tour extends React.Component<TourProps, TourState> {
* @param {ClientRect} rect
* @returns {JSX.Element}
*/
private getDialogForStep(
step: TourScriptStep,
rect: ClientRect,
): JSX.Element {
private getDialogForStep(step: TourScriptStep, rect: ClientRect): JSX.Element {
const buttons = this.getButtons(step);
const size = { width: 400, height: 300 };
const margin = 10;
@@ -167,15 +170,21 @@ export class Tour extends React.Component<TourProps, TourState> {
key={step.name}
isOpen={true}
style={style}
portalClassName="tour-portal"
backdropProps={{ style: { visibility: 'hidden' } }}
portalClassName='tour-portal'
backdropProps={{ style: { visibility: 'hidden' }}}
>
<div className={Classes.DIALOG_HEADER}>
<h4 className={Classes.HEADING}>{step.title}</h4>
<h4 className={Classes.HEADING}>
{step.title}
</h4>
</div>
<div className={Classes.DIALOG_BODY}>
{step.content}
</div>
<div className={Classes.DIALOG_BODY}>{step.content}</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>{buttons}</div>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
{buttons}
</div>
</div>
</Dialog>
);
@@ -196,38 +205,20 @@ export class Tour extends React.Component<TourProps, TourState> {
return (
<>
{this.getDialogForStep(step, rect)}
<svg height="100%" width="100%">
<rect
fill="rgba(0, 0, 0, 0.65)"
x="0"
y="0"
mask="url(#mask)"
height="100%"
width="100%"
/>
<mask
id="mask"
maskUnits="userSpaceOnUse"
maskContentUnits="userSpaceOnUse"
>
<rect
className="bg"
x="0"
y="0"
fill="white"
height="100%"
width="100%"
/>
<svg height='100%' width='100%'>
<rect fill='rgba(0, 0, 0, 0.65)' x='0' y='0' mask='url(#mask)' height='100%' width='100%'/>
<mask id='mask' maskUnits='userSpaceOnUse' maskContentUnits='userSpaceOnUse'>
<rect className='bg' x='0' y='0' fill='white' height='100%' width='100%' />
<rect
x={left}
y={top}
width={width}
height={height}
fill="black"
rx="5"
ry="5"
strokeWidth="5"
stroke="black"
fill='black'
rx='5'
ry='5'
strokeWidth='5'
stroke='black'
/>
</mask>
</svg>
-147
View File
@@ -1,147 +0,0 @@
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>
);
}
}
+2 -21
View File
@@ -3,15 +3,11 @@ import * as path from 'path';
import { MosaicNode } from 'react-mosaic-component';
import { EditorId, MosaicId } from '../interfaces';
import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
// Reminder: When testing, this file is mocked in tests/setup.js
export const USER_DATA_PATH = remote.app.getPath('userData');
export const CONFIG_PATH = path.join(
remote.app.getPath('home'),
'.electron-fiddle',
);
export const CONFIG_PATH = path.join(remote.app.getPath('home'), '.electron-fiddle');
export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
direction: 'row',
@@ -20,22 +16,7 @@ export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
direction: 'column',
first: EditorId.renderer,
second: EditorId.html,
},
};
export const DEFAULT_CLOSED_PANELS: Partial<Record<
MosaicId,
EditorBackup | true
>> = {
docsDemo: true,
preload: getEditorBackup(EditorId.preload),
css: getEditorBackup(EditorId.css),
};
export const EMPTY_EDITOR_CONTENT = {
html: '<!-- Empty -->',
js: '// Empty',
css: '/* Empty */',
}
};
export const ELECTRON_ORG = 'electron';
+4 -7
View File
@@ -42,18 +42,15 @@ 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.getEditorValues({
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) {
const isChanged1x =
(await getContent(EditorId.main, '1.0')) === values.main;
const isChangedOther = (await getContent(EditorId.main)) === values.main;
const isChanged1x = await getContent(EditorId.main, '1.0') === values.main;
const isChangedOther = await getContent(EditorId.main) === values.main;
return isChanged1x || isChangedOther;
} else {
return values[name] === (await getContent(name));
return values[name] === await getContent(name);
}
}
+16 -52
View File
@@ -2,10 +2,9 @@ import * as fsType from 'fs-extra';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import { RunnableVersion, VersionSource } from '../interfaces';
import { ElectronVersion, ElectronVersionSource } 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');
@@ -37,25 +36,6 @@ export async function fetchTypeDefinitions(version: string): Promise<string> {
}
}
/**
* 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
*
@@ -72,9 +52,7 @@ export function getOfflineTypeDefinitionPath(version: string): string {
* @param {string} version
* @returns {boolean}
*/
export async function getOfflineTypeDefinitions(
version: string,
): Promise<boolean> {
export async function getOfflineTypeDefinitions(version: string): Promise<boolean> {
const fs = await fancyImport<typeof fsType>('fs-extra');
return fs.existsSync(getOfflineTypeDefinitionPath(version));
}
@@ -86,12 +64,10 @@ export async function getOfflineTypeDefinitions(
* @param {string} version
* @returns {void}
*/
export async function getDownloadedVersionTypeDefs(
version: RunnableVersion,
): Promise<string | null> {
export async function getDownloadedVersionTypeDefs(version: ElectronVersion): Promise<string | null> {
const fs = await fancyImport<typeof fsType>('fs-extra');
await fs.mkdirp(definitionPath);
const offlinePath = getOfflineTypeDefinitionPath(version.version);
await fs.mkdirp(definitionPath);
const offlinePath = getOfflineTypeDefinitionPath(version.version);
if (await getOfflineTypeDefinitions(version.version)) {
try {
@@ -115,8 +91,8 @@ export async function getDownloadedVersionTypeDefs(
}
}
export async function getLocalVersionTypeDefs(version: RunnableVersion) {
if (version.source === VersionSource.local && !!version.localPath) {
export async function getLocalVersionTypeDefs(version: ElectronVersion) {
if (version.source === ElectronVersionSource.local && !!version.localPath) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const typesPath = getLocalTypePathForVersion(version);
if (!!typesPath && fs.existsSync(typesPath)) {
@@ -131,10 +107,7 @@ export async function getLocalVersionTypeDefs(version: RunnableVersion) {
*
* @param {string} version
*/
export async function updateEditorTypeDefinitions(
version: RunnableVersion,
i = 0,
): Promise<void> {
export async function updateEditorTypeDefinitions(version: ElectronVersion, i: number = 0): Promise<void> {
const defer = async (): Promise<void> => {
if (i > 10) {
console.warn(`Fetch Types: Failed, dependencies do not exist`);
@@ -142,23 +115,18 @@ export async function updateEditorTypeDefinitions(
}
console.warn(`Fetch Types: Called too soon, deferring`);
return callIn(i * 100 + 200, () =>
updateEditorTypeDefinitions(version, i + 1),
);
return callIn(i * 100 + 200, () => updateEditorTypeDefinitions(version, i + 1));
};
// If this method is called before we're ready, we'll delay this work a bit
if (!window.ElectronFiddle.app || !window.ElectronFiddle.app.monaco)
return defer();
if (!window.ElectronFiddle.app || !window.ElectronFiddle.app.monaco) return defer();
const { app } = window.ElectronFiddle;
const monaco: typeof MonacoType = app.monaco!;
const typeDefDisposable: MonacoType.IDisposable = app.typeDefDisposable!;
const getTypeDefs =
version.source === VersionSource.local
? getLocalVersionTypeDefs
: getDownloadedVersionTypeDefs;
const getTypeDefs = (version.source === ElectronVersionSource.local) ?
getLocalVersionTypeDefs : getDownloadedVersionTypeDefs;
const typeDefs = await getTypeDefs(version);
@@ -167,19 +135,15 @@ export async function updateEditorTypeDefinitions(
}
if (typeDefs) {
console.log(
`Fetch Types: Updating Monaco types with electron.d.ts@${version.version}`,
);
const disposable = monaco.languages.typescript.javascriptDefaults.addExtraLib(
typeDefs,
);
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.version} 😢`);
}
}
export function getLocalTypePathForVersion(version: RunnableVersion) {
export function getLocalTypePathForVersion(version: ElectronVersion) {
if (version.localPath) {
return path.join(
version.localPath,
@@ -187,7 +151,7 @@ export function getLocalTypePathForVersion(version: RunnableVersion) {
'electron',
'tsc',
'typings',
'electron.d.ts',
'electron.d.ts'
);
} else {
return null;
+14 -40
View File
@@ -3,14 +3,7 @@ import * as path from 'path';
import { EditorValues, Files, FileTransform } from '../interfaces';
import { IpcEvents } from '../ipc-events';
import {
INDEX_HTML_NAME,
MAIN_JS_NAME,
PACKAGE_NAME,
PRELOAD_JS_NAME,
RENDERER_JS_NAME,
STYLES_CSS_NAME,
} from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { DEFAULT_OPTIONS, PackageJsonOptions } from '../utils/get-package';
import { fancyImport } from '../utils/import';
import { ipcRendererManager } from './ipc';
@@ -24,11 +17,6 @@ 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);
});
@@ -41,12 +29,9 @@ export class FileManager {
this.saveFiddle(filePath, dotfilesTransform);
});
ipcRendererManager.on(
IpcEvents.FS_SAVE_FIDDLE_FORGE,
(_event, filePath) => {
this.saveFiddle(filePath, dotfilesTransform, forgeTransform);
},
);
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_FORGE, (_event, filePath) => {
this.saveFiddle(filePath, dotfilesTransform, forgeTransform);
});
}
/**
@@ -57,9 +42,7 @@ export class FileManager {
*/
public async openTemplate(templateName: string) {
const editorValues = await getTemplateValues(templateName);
await window.ElectronFiddle.app.replaceFiddle(editorValues, {
templateName,
});
await window.ElectronFiddle.app.replaceFiddle(editorValues, {templateName});
}
/**
@@ -80,10 +63,10 @@ export class FileManager {
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)),
};
window.ElectronFiddle.app.replaceFiddle(editorValues, { filePath });
window.ElectronFiddle.app.replaceFiddle(editorValues, {filePath});
}
/**
@@ -93,10 +76,7 @@ export class FileManager {
* @param {string} filePath
* @memberof FileManager
*/
public async saveFiddle(
filePath?: string,
...transforms: Array<FileTransform>
) {
public async saveFiddle(filePath?: string, ...transforms: Array<FileTransform>) {
const { localPath } = this.appState;
const pathToSave = filePath || localPath;
@@ -121,13 +101,14 @@ export class FileManager {
if (pathToSave !== localPath) {
this.appState.localPath = pathToSave;
this.appState.gistId = undefined;
}
this.appState.isUnsaved = false;
window.ElectronFiddle.app.setupUnsavedOnChangeListener();
}
}
/**
* Get files to save, but with a transform applied
*
@@ -136,10 +117,7 @@ export class FileManager {
* @returns {Promise<Files>}
* @memberof FileManager
*/
public async getFiles(
options?: PackageJsonOptions,
...transforms: Array<FileTransform>
): Promise<Files> {
public async getFiles(options?: PackageJsonOptions, ...transforms: Array<FileTransform>): Promise<Files> {
const pOptions = typeof options === 'object' ? options : DEFAULT_OPTIONS;
const values = await window.ElectronFiddle.app.getEditorValues(pOptions);
let output: Files = new Map();
@@ -148,7 +126,6 @@ export class FileManager {
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) {
@@ -156,16 +133,14 @@ export class FileManager {
console.log(`getFiles: Applying ${transform.name}`);
output = await transform(output);
} catch (error) {
console.warn(`getFiles: Failed to apply transform`, {
transform,
error,
});
console.warn(`getFiles: Failed to apply transform`, { transform, error });
}
}
return output;
}
/**
* Attempts to clean a given directory. Used to manually
* clean temp directories.
@@ -198,8 +173,7 @@ export class FileManager {
* @returns {Promise<string>}
*/
public async saveToTemp(
options: PackageJsonOptions,
...transforms: Array<FileTransform>
options: PackageJsonOptions, ...transforms: Array<FileTransform>
): Promise<string> {
const fs = await fancyImport<typeof fsType>('fs-extra');
const tmp = await import('tmp');
-1
View File
@@ -15,7 +15,6 @@ export class IpcRendererManager extends EventEmitter {
super();
ipcRendererEvents.forEach((name) => {
ipcRenderer.removeAllListeners(name);
ipcRenderer.on(name, (...args: Array<any>) => this.emit(name, ...args));
});
}
+33 -42
View File
@@ -1,13 +1,8 @@
import { EditorValues } from '../interfaces';
import { exec } from '../utils/exec';
const { builtinModules } = require('module');
export type IPackageManager = 'npm' | 'yarn';
export interface PMOperationOptions {
export interface NpmOperationOptions {
dir: string;
packageManager: IPackageManager;
}
export let isInstalled: boolean | null = null;
@@ -16,39 +11,41 @@ export let isInstalled: boolean | null = null;
/* perhaps we can expose this to the settings module?*/
const ignoredModules: Array<string> = [
'electron',
'original-fs',
...builtinModules,
// tslint:disable-next-line:no-submodule-imports
...require('builtin-modules/static')
];
/* regular expression to both match and extract module names */
const requiregx = /^.*require\(['"](.*?)['"]\)/gm;
/*
Quick and dirty filter functions for filtering module names
*/
const isIgnored = (str: string): boolean => ignoredModules.includes(str);
const isLocalModule = (str: string): boolean => /^[,/~\.]/.test(str);
const isLocalModule = (str: string): boolean => (/^[,/~\.]/.test(str));
const isUnique = (item: any, idx: number, arr: Array<any>): boolean => {
return arr.lastIndexOf(item) === idx;
};
/**
* Checks if npm is installed by checking if a binary
* with that name can be found.
*/
export async function getIsNpmInstalled(
ignoreCache?: boolean,
): Promise<boolean> {
export async function getIsNpmInstalled(ignoreCache?: boolean): Promise<boolean> {
if (isInstalled !== null && !ignoreCache) return isInstalled;
const command = process.platform === 'win32' ? 'where.exe npm' : 'which npm';
const command = process.platform === 'win32'
? 'where.exe npm'
: 'which npm';
try {
await exec(process.cwd(), command);
return (isInstalled = true);
return isInstalled = true;
} catch (error) {
console.warn(`getIsNpmInstalled: "${command}" failed.`, error);
return (isInstalled = false);
return isInstalled = false;
}
}
@@ -59,10 +56,13 @@ export async function getIsNpmInstalled(
* @returns {Array<string>}
*/
export function findModulesInEditors(values: EditorValues) {
const files = [values.main, values.renderer];
const files = [ values.main, values.renderer ];
const modules = files.reduce(
(agg, file) => [...agg, ...findModules(file)],
[],
(agg, file) => [
...agg,
...findModules(file)
],
[]
);
console.log('Modules Found:', modules);
@@ -89,7 +89,8 @@ export function findModules(input: string): Array<string> {
let match: RegExpMatchArray | null;
/* grab all global require matches in the text */
while ((match = requiregx.exec(input) || null)) {
// tslint:disable-next-line:no-conditional-assignment
while (match = (requiregx.exec(input) || null)) {
// ensure commented-out requires aren't downloaded
if (!match[0].startsWith('//')) {
const mod = match[1];
@@ -100,7 +101,9 @@ export function findModules(input: string): Array<string> {
/* map and reduce */
return modules
.map((mod) =>
mod.includes('/') && !mod.startsWith('@') ? mod.split('/')[0] : mod,
mod.includes('/') && !mod.startsWith('@') ?
mod.split('/')[0] :
mod
)
.filter((m) => !isIgnored(m))
.filter((m) => !isLocalModule(m))
@@ -110,37 +113,25 @@ export function findModules(input: string): Array<string> {
/**
* Installs given modules to a given folder.
*
* @param {PMOperationOptions} { dir, packageManager }
* @param {NpmOperationOptions} { dir }
* @param {...Array<string>} names
* @returns {Promise<string>}
*/
export async function installModules(
{ dir, packageManager }: PMOperationOptions,
...names: Array<string>
): Promise<string> {
let nameArgs: Array<string> = [];
export async function installModules({ dir }: NpmOperationOptions, ...names: Array<string>): Promise<string> {
const nameArgs = names.length > 0
? [ '-S', ...names ]
: ['--dev --prod'];
if (packageManager === 'npm') {
nameArgs = names.length > 0 ? ['-S', ...names] : ['--dev --prod'];
} else {
nameArgs = [...names];
}
const installCommand = packageManager === 'npm' ? 'npm install' : 'yarn add';
return exec(dir, [installCommand].concat(nameArgs).join(' '));
return exec(dir, [ `npm install` ].concat(nameArgs).join(' '));
}
/**
* Execute an "{packageManager} run" command
* Execute an "npm run" command
*
* @param {PMOperationOptions} { dir, packageManager }
* @param {NpmOperationOptions} { dir }
* @param {string} command
* @returns {Promise<string>}
*/
export function packageRun(
{ dir, packageManager }: PMOperationOptions,
command: string,
): Promise<string> {
return exec(dir, `${packageManager} run ${command}`);
export function npmRun({ dir }: NpmOperationOptions, command: string): Promise<string> {
return exec(dir, `npm run ${command}`);
}
+54 -126
View File
@@ -1,15 +1,9 @@
import { Octokit } from '@octokit/rest';
import * as 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 { EditorId, EditorValues } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { getOctokit } from '../utils/octokit';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import { sortedElectronMap } from '../utils/sorted-map';
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
import { getContent } from './content';
import { AppState } from './state';
@@ -29,33 +23,22 @@ export class RemoteLoader {
this.handleLoadingFailed.bind(this);
}
public async loadFiddleFromElectronExample(
_: any,
exampleInfo: { path: string; ref: string },
) {
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}`,
);
const ok = await this.verifyRemoteLoad('example from the Electron docs', exampleInfo.ref);
if (!ok) return;
this.fetchExampleAndLoad(ref, path);
this.fetchExampleAndLoad(exampleInfo.ref, exampleInfo.path);
}
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
const { id } = gistInfo;
const ok = await this.verifyRemoteLoad(`gist`);
const ok = await this.verifyRemoteLoad('gist');
if (!ok) return;
this.fetchGistAndLoad(id);
this.fetchGistAndLoad(gistInfo.id);
}
public async fetchExampleAndLoad(
ref: string,
path: string,
): Promise<boolean> {
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
try {
const octo = await getOctokit(this.appState);
@@ -74,14 +57,11 @@ export class RemoteLoader {
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',
);
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
}
for (const child of folder.data) {
@@ -92,52 +72,27 @@ export class RemoteLoader {
switch (child.name) {
case MAIN_JS_NAME:
loaders.push(
fetch(child.download_url)
.then((r) => r.text())
.then((t) => {
values.main = t;
}),
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;
}),
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;
}),
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;
}),
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.preload = t; })
);
break;
@@ -162,10 +117,7 @@ export class RemoteLoader {
* @returns {string}
* @memberof RemoteLoader
*/
public getContentOrEmpty(
gist: Octokit.Response<Octokit.GistsGetResponse>,
name: string,
): string {
public getContentOrEmpty(gist: Octokit.Response<Octokit.GistsGetResponse>, name: string): string {
try {
return gist.data.files[name].content;
} catch (error) {
@@ -184,16 +136,12 @@ export class RemoteLoader {
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,
);
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)
}, gistId);
} catch (error) {
return this.handleLoadingFailed(error);
}
@@ -202,27 +150,20 @@ export class RemoteLoader {
public async setElectronVersionWithRef(ref: string): Promise<boolean> {
const version = await this.getPackageVersionFromRef(ref);
const supportedVersions = sortedElectronMap(
this.appState.versions,
(k) => k,
);
const supportedVersions = sortedElectronMap(this.appState.versions, (k) => k);
if (!supportedVersions.includes(version)) {
this.handleLoadingFailed(
new Error('Version of Electron in example not supported'),
);
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,
);
const versionReleaseChannel: ElectronReleaseChannel = getReleaseChannel(version);
if (!this.appState.channelsToShow.includes(versionReleaseChannel)) {
if (!this.appState.versionsToShow.includes(versionReleaseChannel)) {
const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
if (!ok) return false;
this.appState.channelsToShow.push(versionReleaseChannel);
this.appState.versionsToShow.push(versionReleaseChannel);
}
this.appState.setVersion(version);
@@ -235,23 +176,17 @@ export class RemoteLoader {
owner: ELECTRON_ORG,
repo: ELECTRON_REPO,
ref,
path: 'package.json',
path: 'package.json'
});
if (!Array.isArray(packageJsonData) && !!packageJsonData.content) {
const packageJsonString = Buffer.from(
packageJsonData.content,
'base64',
).toString('utf8');
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,
},
);
console.error(`getPackageVersionFromRef: Received unexpected response from GitHub, could not parse version`, {
packageJsonData
});
return '0.0.0';
}
@@ -262,28 +197,26 @@ export class RemoteLoader {
*
* @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.`,
public async verifyRemoteLoad(what: string, fiddlePath?: string): Promise<boolean> {
this.appState.setConfirmationPromptTexts({
label: `Are you sure you sure you want to load this '${what}' from fiddle path '${fiddlePath}'? Only load and run it if you trust the source.`
});
this.appState.isGenericDialogShowing = true;
await when(() => !this.appState.isGenericDialogShowing);
this.appState.isConfirmationPromptShowing = true;
await when(() => !this.appState.isConfirmationPromptShowing);
return !!this.appState.genericDialogLastResult;
return !!this.appState.confirmationPromptLastResult;
}
public async verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
this.appState.setWarningDialogTexts({
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?`,
version of Electron from the example?`
});
this.appState.isGenericDialogShowing = true;
await when(() => !this.appState.isGenericDialogShowing);
this.appState.isWarningDialogShowing = true;
await when(() => !this.appState.isWarningDialogShowing);
return !!this.appState.genericDialogLastResult;
return !!this.appState.warningDialogLastResult;
}
/**
@@ -293,11 +226,8 @@ export class RemoteLoader {
* @param {string} gistId
* @returns {boolean}
*/
private async handleLoadingSuccess(
values: Partial<EditorValues>,
gistId: string,
): Promise<boolean> {
await window.ElectronFiddle.app.replaceFiddle(values, { gistId });
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
await window.ElectronFiddle.app.replaceFiddle(values, {gistId});
return true;
}
@@ -310,20 +240,18 @@ export class RemoteLoader {
*/
private handleLoadingFailed(error: Error): false {
if (navigator.onLine) {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
this.appState.setWarningDialogTexts({
label: `Loading the fiddle failed: ${error}`,
cancel: undefined,
cancel: undefined
});
} else {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
this.appState.setWarningDialogTexts({
label: `Loading the fiddle failed. Your computer seems to be offline. Error: ${error}`,
cancel: undefined,
cancel: undefined
});
}
this.appState.toggleGenericDialog();
this.appState.toggleWarningDialog();
console.warn(`Loading Fiddle failed`, error);
return false;
+36 -74
View File
@@ -3,22 +3,16 @@ import * as path from 'path';
import { EditorValues, FileTransform } from '../interfaces';
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,
packageRun,
PMOperationOptions,
} from './npm';
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from './npm';
import { AppState } from './state';
export enum ForgeCommands {
PACKAGE = 'package',
MAKE = 'make',
MAKE = 'make'
}
export class Runner {
@@ -28,10 +22,6 @@ 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);
@@ -49,7 +39,7 @@ export class Runner {
public async run(): Promise<boolean> {
const { fileManager, getEditorValues } = window.ElectronFiddle.app;
const options = { includeDependencies: false, includeElectron: false };
const { currentElectronVersion } = this.appState;
const { binaryManager, currentElectronVersion } = this.appState;
const { version, localPath } = currentElectronVersion;
if (this.appState.isClearingConsoleOnRun) {
@@ -59,19 +49,18 @@ export class Runner {
const values = await getEditorValues(options);
const dir = await this.saveToTemp(options);
const packageManager = this.appState.packageManager;
if (!dir) return false;
try {
await this.installModulesForEditor(values, { dir, packageManager });
await this.installModulesForEditor(values, dir);
} catch (error) {
console.error('Runner: Could not install modules', error);
fileManager.cleanup(dir);
return false;
}
const isReady = await getIsDownloaded(version, localPath);
const isReady = await binaryManager.getIsDownloaded(version, localPath);
if (!isReady) {
console.warn(`Runner: Binary ${version} not ready`);
@@ -108,18 +97,15 @@ export class Runner {
* @returns {Promise<boolean>}
* @memberof Runner
*/
public async performForgeOperation(
operation: ForgeCommands,
): Promise<boolean> {
public async performForgeOperation(operation: ForgeCommands): Promise<boolean> {
const options = { includeDependencies: true, includeElectron: true };
const { dotfilesTransform } = await import('./transforms/dotfiles');
const { forgeTransform } = await import('./transforms/forge');
const { pushError, pushOutput } = this.appState;
const strings =
operation === ForgeCommands.MAKE
? ['Creating installers for', 'Binary']
: ['Packaging', 'Installers'];
const strings = operation === ForgeCommands.MAKE
? [ 'Creating installers for', 'Binary' ]
: [ 'Packaging', 'Installers' ];
this.appState.isConsoleShowing = true;
pushOutput(`📦 ${strings[0]} current Fiddle...`);
@@ -134,22 +120,16 @@ export class Runner {
}
// Save files to temp
const dir = await this.saveToTemp(
options,
dotfilesTransform,
forgeTransform,
);
const dir = await this.saveToTemp(options, dotfilesTransform, forgeTransform);
if (!dir) return false;
const packageManager = this.appState.packageManager;
// Files are now saved to temp, let's install Forge and dependencies
if (!(await this.packageInstall({ dir, packageManager }))) return false;
if (!(await this.npmInstall(dir))) return false;
// Cool, let's run "package"
try {
console.log(`Now creating ${strings[1].toLowerCase()}...`);
pushOutput(await packageRun({ dir, packageManager }, operation));
pushOutput(await npmRun({ dir }, operation));
pushOutput(`${strings[1]} successfully created.`, { isNotPre: true });
} catch (error) {
pushError(`Creating ${strings[1].toLowerCase()} failed.`, error);
@@ -169,18 +149,13 @@ export class Runner {
* @param {string} dir
* @returns {Promise<void>}
*/
public async installModulesForEditor(
values: EditorValues,
pmOptions: PMOperationOptions,
): Promise<void> {
const modules = findModulesInEditors(values);
public async installModulesForEditor(values: EditorValues, dir: string): Promise<void> {
const modules = await findModulesInEditors(values);
const { pushOutput } = this.appState;
if (modules && modules.length > 0) {
if (!(await getIsNpmInstalled())) {
let message = `The ${maybePlural(`module`, modules)} ${modules.join(
', ',
)} need to be installed, `;
let message = `The ${maybePlural(`module`, modules)} ${modules.join(', ')} need to be installed, `;
message += `but we could not find npm. Fiddle requires Node.js and npm `;
message += `to support the installation of modules not included in `;
message += `Electron. Please visit https://nodejs.org to install Node.js `;
@@ -190,13 +165,8 @@ export class Runner {
return;
}
pushOutput(
`Installing node modules using ${
pmOptions.packageManager
}: ${modules.join(', ')}...`,
{ isNotPre: true },
);
pushOutput(await installModules(pmOptions, ...modules));
pushOutput(`Installing npm modules: ${modules.join(', ')}...`, { isNotPre: true });
pushOutput(await installModules({ dir }, ...modules));
}
}
@@ -209,38 +179,33 @@ export class Runner {
* @memberof Runner
*/
public async execute(dir: string): Promise<void> {
const { currentElectronVersion, pushOutput } = this.appState;
const { currentElectronVersion, pushOutput, binaryManager } = this.appState;
const { version, localPath } = currentElectronVersion;
const binaryPath = getElectronBinaryPath(version, localPath);
const binaryPath = binaryManager.getElectronBinaryPath(version, localPath);
console.log(`Runner: Binary ${binaryPath} ready, launching`);
const env = { ...process.env };
if (this.appState.isEnablingElectronLogging) {
env.ELECTRON_ENABLE_LOGGING = 'true';
env.ELECTRON_DEBUG_NOTIFICATIONS = 'true';
env.ELECTRON_ENABLE_STACK_DUMPING = 'true';
} else {
delete env.ELECTRON_ENABLE_LOGGING;
delete env.ELECTRON_DEBUG_NOTIFICATIONS;
delete env.ELECTRON_ENABLE_STACK_DUMPING;
}
// 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.child = spawn(binaryPath, [ dir, '--inspect' ], {
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()}.` : `.`;
const withCode = typeof code === 'number'
? ` with code ${code.toString()}.`
: `.`;
pushOutput(`Electron exited${withCode}`);
this.appState.isRunning = false;
@@ -261,8 +226,7 @@ export class Runner {
* @memberof Runner
*/
public async saveToTemp(
options: PackageJsonOptions,
...transforms: Array<FileTransform>
options: PackageJsonOptions, ...transforms: Array<FileTransform>
): Promise<string | null> {
const { fileManager } = window.ElectronFiddle.app;
const { pushOutput, pushError } = this.appState;
@@ -281,16 +245,16 @@ export class Runner {
/**
* Installs modules in a given directory (we're basically
* just running "{packageManager} install")
* just running "npm install")
*
* @param {PMOperationOptions} options
* @param {string} dir
* @returns
* @memberof Runner
*/
public async packageInstall(options: PMOperationOptions): Promise<boolean> {
public async npmInstall(dir: string): Promise<boolean> {
try {
this.appState.pushOutput(`Now running "npm install..."`);
this.appState.pushOutput(await installModules(options));
this.appState.pushOutput(await installModules({ dir }));
return true;
} catch (error) {
this.appState.pushError('Failed to run "npm install".', error);
@@ -304,14 +268,12 @@ export class Runner {
*/
private async deleteUserData() {
if (this.appState.isKeepingUserDataDirs) {
console.log(
`Cleanup: Not deleting data dir due to isKeepingUserDataDirs setting`,
);
console.log(`Cleanup: Not deleting data dir due to isKeepingUserDataDirs setting`);
return;
}
const name = await this.appState.getName();
const appData = path.join(this.appState.appData, name);
const appData = getAppDataDir(name);
console.log(`Cleanup: Deleting data dir ${appData}`);
await window.ElectronFiddle.app.fileManager.cleanup(appData);
+199 -299
View File
@@ -6,75 +6,59 @@ import {
ALL_MOSAICS,
DocsDemoPage,
EditorId,
GenericDialogOptions,
GenericDialogType,
GistActionState,
ElectronVersion,
ElectronVersionSource,
MosaicId,
NodeVersion,
NpmVersion,
OutputEntry,
OutputOptions,
RunnableVersion,
Version,
VersionSource,
VersionState,
WarningDialogTexts
} 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 { 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 {
getDownloadedVersions,
getDownloadingVersions,
removeBinary,
setupBinary,
} from './binary';
import { BinaryManager } from './binary';
import { Bisector } from './bisect';
import { DEFAULT_CLOSED_PANELS, DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
import { DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
import { getContent, isContentUnchanged } from './content';
import {
getLocalTypePathForVersion,
updateEditorTypeDefinitions,
} from './fetch-types';
import { getLocalTypePathForVersion, updateEditorTypeDefinitions } from './fetch-types';
import { ipcRendererManager } from './ipc';
import { activateTheme } from './themes';
import { waitForEditorsToMount } from '../utils/editor-mounted';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import { IPackageManager } from './npm';
import {
addLocalVersion,
ElectronReleaseChannel,
getAllElectronVersions,
getDefaultVersion,
getElectronVersions,
getReleaseChannel,
getUpdatedElectronVersions,
saveLocalVersions,
getUpdatedNodeVersions,
saveLocalVersions
} from './versions';
const knownVersions = getElectronVersions();
const knownVersions = getAllElectronVersions();
const defaultVersion = getDefaultVersion(knownVersions);
const defaultNodeVersion = '12.0.0';
/**
* Editors exist outside of React's world. To make things *a lot*
* easier, we keep them around in a global object. Don't judge us,
* we're really only doing that for the editors.
*/
window.ElectronFiddle =
window.ElectronFiddle ||
({
editors: {
main: null,
renderer: null,
html: null,
},
app: null,
} as any);
window.ElectronFiddle = window.ElectronFiddle || {
editors: {
main: null,
renderer: null,
html: null
},
app: null
} as any;
/**
* The application's state. Exported as a singleton below.
@@ -85,98 +69,65 @@ window.ElectronFiddle =
export class AppState {
// -- Persisted settings ------------------
@observable public version: string = defaultVersion;
@observable public nodeVersion: string = defaultNodeVersion;
@observable public theme: string | null = localStorage.getItem('theme');
@observable public gitHubAvatarUrl: string | null = localStorage.getItem(
'gitHubAvatarUrl',
);
@observable public gitHubName: string | null = localStorage.getItem(
'gitHubName',
);
@observable public gitHubLogin: string | null = localStorage.getItem(
'gitHubLogin',
);
@observable public gitHubToken: string | null =
localStorage.getItem('gitHubToken') || null;
@observable public gitHubPublishAsPublic = !!this.retrieve(
'gitHubPublishAsPublic',
);
@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 = !!this.retrieve(
'isKeepingUserDataDirs',
);
@observable public isEnablingElectronLogging = !!this.retrieve(
'isEnablingElectronLogging',
);
@observable public isClearingConsoleOnRun = !!this.retrieve(
'isClearingConsoleOnRun',
);
@observable public executionFlags: Array<string> =
(this.retrieve('executionFlags') as Array<string>) === null
? []
: (this.retrieve('executionFlags') as Array<string>);
@observable public packageManager: IPackageManager =
(localStorage.getItem('packageManager') as IPackageManager) || 'npm';
@observable public gitHubAvatarUrl: string | null = localStorage.getItem('gitHubAvatarUrl');
@observable public gitHubName: string | null = localStorage.getItem('gitHubName');
@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<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 isClearingConsoleOnRun: boolean = !!this.retrieve('isClearingConsoleOnRun');
// -- Various session-only state ------------------
@observable public gistId: string | undefined;
@observable public versions: Record<
string,
RunnableVersion
> = arrayToStringMap(knownVersions);
@observable public gistId: string = '';
@observable public versions: Record<string, ElectronVersion> = arrayToStringMap(knownVersions);
@observable public nodeVersions: Record<string, NodeVersion> = arrayToStringMap([] as Array<NodeVersion>);
@observable public output: Array<OutputEntry> = [];
@observable public localPath: string | undefined;
@observable public genericDialogOptions = {
type: GenericDialogType.warning,
label: '' as string | JSX.Element,
ok: 'Okay',
cancel: 'Cancel',
};
@observable public genericDialogLastResult: boolean | null = null;
@observable public mosaicArrangement: MosaicNode<
MosaicId
> | null = DEFAULT_MOSAIC_ARRANGEMENT;
@observable public warningDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public confirmationDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public warningDialogLastResult: boolean | null = null;
@observable public confirmationPromptLastResult: 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 binaryManager: BinaryManager = new BinaryManager();
@observable public Bisector: Bisector | undefined;
@observable public activeGistAction: GistActionState = GistActionState.none;
@observable public isRunning = false;
@observable public isUnsaved: boolean;
@observable public isUpdatingElectronVersions = false;
@observable public isQuitting = false;
@observable public isPublishing: boolean = false;
@observable public isRunning: boolean = false;
@observable public isUnsaved: boolean = false;
@observable public isUpdatingElectronVersions: boolean = false;
// -- Various "isShowing" settings ------------------
@observable public isBisectCommandShowing: boolean;
@observable public isConsoleShowing = false;
@observable public isTokenDialogShowing = false;
@observable public isGenericDialogShowing = false;
@observable public isSettingsShowing = false;
@observable public isBisectDialogShowing = false;
@observable public isAddVersionDialogShowing = false;
@observable public isThemeDialogShowing = false;
@observable public isTourShowing = !localStorage.getItem('hasShownTour');
@observable public isConsoleShowing: boolean = false;
@observable public isTokenDialogShowing: boolean = false;
@observable public isWarningDialogShowing: boolean = false;
@observable public isConfirmationPromptShowing: boolean = false;
@observable public isSettingsShowing: boolean = false;
@observable public isBisectDialogShowing: boolean = false;
@observable public isAddVersionDialogShowing: boolean = false;
@observable public isThemeDialogShowing: boolean = false;
@observable public isNodeMode: 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>
> = DEFAULT_CLOSED_PANELS;
@observable public closedPanels: Partial<Record<MosaicId, EditorBackup | true>> = {
docsDemo: true // Closed by default
};
private outputBuffer = '';
private outputBuffer: string = '';
private name: string;
public appData: string;
constructor() {
// Bind all actions
@@ -185,119 +136,85 @@ export class AppState {
this.pushOutput = this.pushOutput.bind(this);
this.removeVersion = this.removeVersion.bind(this);
this.setVersion = this.setVersion.bind(this);
this.setNodeVersion = this.setNodeVersion.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.toggleNodeMode = this.toggleNodeMode.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);
this.resetEditorLayout = this.resetEditorLayout.bind(this);
this.setIsQuitting = this.setIsQuitting.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,
);
ipcRendererManager.on(IpcEvents.BEFORE_QUIT, this.setIsQuitting);
ipcRendererManager.once(IpcEvents.SET_APPDATA_DIR, (_event, dir) => {
this.appData = dir;
});
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('isClearingConsoleOnRun', this.isClearingConsoleOnRun));
autorun(() => this.save('gitHubAvatarUrl', this.gitHubAvatarUrl));
autorun(() => this.save('gitHubLogin', this.gitHubLogin));
autorun(() => this.save('gitHubName', this.gitHubName));
autorun(() => this.save('gitHubToken', this.gitHubToken));
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('gitHubPublishAsPublic', this.gitHubPublishAsPublic));
autorun(() => this.save('isKeepingUserDataDirs', this.isKeepingUserDataDirs));
autorun(() => this.save('isEnablingElectronLogging', this.isEnablingElectronLogging));
autorun(() => this.save('version', this.version));
autorun(() => this.save('channelsToShow', this.channelsToShow));
autorun(() => this.save('versionsToShow', this.versionsToShow));
autorun(() => this.save('statesToShow', this.statesToShow));
autorun(() => this.save('packageManager', this.packageManager ?? 'npm'));
autorun(() => {
if (typeof this.isUnsaved === 'undefined') return;
if (!!this.isUnsaved) {
if (this.isUnsaved) {
window.onbeforeunload = () => {
ipcRendererManager.send(IpcEvents.SHOW_INACTIVE);
this.setGenericDialogOptions({
type: GenericDialogType.warning,
this.setWarningDialogTexts({
label: `The current Fiddle is unsaved. Do you want to exit anyway?`,
ok: 'Quit',
ok: 'Quit'
});
this.isGenericDialogShowing = true;
this.isWarningDialogShowing = true;
// We'll wait until the warning dialog was closed
when(() => !this.isGenericDialogShowing).then(() => {
const quitConfirmed = this.genericDialogLastResult;
when(() => !this.isWarningDialogShowing).then(() => {
// The user confirmed, let's close for real.
if (quitConfirmed) {
if (this.warningDialogLastResult) {
window.onbeforeunload = null;
window.close();
}
// isQuitting checks if we're trying to quit the app
// or just close the window
if (this.isQuitting) {
ipcRendererManager.send(IpcEvents.CONFIRM_QUIT, quitConfirmed);
this.isQuitting = false;
// Should we just close or quit?
const remote = require('electron').remote;
const isQuitting = remote.getGlobal('isQuitting');
if (isQuitting) {
remote.app.quit();
} else {
window.close();
}
}
});
// return value doesn't matter, we just want to cancel the event
return false;
};
} 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();
});
});
}
});
// Update our known versions
this.updateElectronVersions();
this.updateNodeVersions();
// Make sure the console isn't all empty and sad
this.pushOutput('Console ready 🔬');
}
/**
* Returns the current RunnableVersion or the first
* Returns the current ElectronVersion or the first
* one that can be found.
*/
@computed get currentElectronVersion(): RunnableVersion {
@computed get currentElectronVersion(): ElectronVersion {
if (this.versions[this.version]) {
return this.versions[this.version];
} else {
@@ -306,30 +223,15 @@ export class AppState {
}
/**
* Returns an array of Electron versions to show given the
* current settings for states and channels to display
* Returns the current NodeVersion or the first
* one that can be found.
*/
@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;
});
@computed get currentNodeVersion(): NodeVersion {
if (this.nodeVersions[this.nodeVersion]) {
return this.nodeVersions[this.nodeVersion];
} else {
return this.nodeVersions[defaultNodeVersion];
}
}
/**
@@ -352,6 +254,21 @@ export class AppState {
this.isUpdatingElectronVersions = false;
}
/**
* Update the Node versions: First, fetch them from Node.js,
* then update their respective downloaded state.
*
* Fails silently.
*/
@action public async updateNodeVersions() {
try {
const nodeVersions = await getUpdatedNodeVersions();
this.nodeVersions = arrayToStringMap(nodeVersions);
} catch (error) {
console.warn(`State: Could not update Node.js versions`, error);
}
}
@action public async getName() {
if (!this.name) {
this.name = await getName(this);
@@ -364,6 +281,10 @@ export class AppState {
this.isConsoleShowing = !this.isConsoleShowing;
}
@action public toggleNodeMode() {
this.isNodeMode = !this.isNodeMode;
}
@action public clearConsole() {
this.output = [];
}
@@ -387,11 +308,11 @@ export class AppState {
this.isTokenDialogShowing = !this.isTokenDialogShowing;
}
@action public toggleGenericDialog() {
this.isGenericDialogShowing = !this.isGenericDialogShowing;
@action public toggleWarningDialog() {
this.isWarningDialogShowing = !this.isWarningDialogShowing;
if (this.isGenericDialogShowing) {
this.genericDialogLastResult = null;
if (this.isWarningDialogShowing) {
this.warningDialogLastResult = null;
}
}
@@ -399,18 +320,24 @@ export class AppState {
this.isBisectDialogShowing = !this.isBisectDialogShowing;
}
@action public toggleConfirmationPromptDialog() {
this.isConfirmationPromptShowing = !this.isConfirmationPromptShowing;
if (this.isConfirmationPromptShowing) {
this.confirmationPromptLastResult = null;
}
}
@action public toggleSettings() {
// We usually don't lose editor focus,
// so you can still type. Let's force-blur.
(document.activeElement as HTMLInputElement).blur();
if ((document.activeElement as HTMLInputElement).blur) {
(document.activeElement as HTMLInputElement).blur();
}
this.resetView({ isSettingsShowing: !this.isSettingsShowing });
}
@action public setIsQuitting() {
this.isQuitting = true;
}
@action public disableTour() {
this.resetView({ isTourShowing: false });
localStorage.setItem('hasShownTour', 'true');
@@ -426,18 +353,26 @@ export class AppState {
window.ElectronFiddle.app.setupTheme();
}
@action public setGenericDialogOptions(opts: GenericDialogOptions) {
this.genericDialogOptions = {
@action public setWarningDialogTexts(input: WarningDialogTexts) {
this.warningDialogTexts = {
ok: 'Okay',
cancel: 'Cancel',
...opts,
...input
};
}
@action public addLocalVersion(input: Version) {
@action public setConfirmationPromptTexts(input: WarningDialogTexts) {
this.confirmationDialogTexts = {
ok: 'Okay',
cancel: 'Cancel',
...input
};
}
@action public addLocalVersion(input: NpmVersion) {
addLocalVersion(input);
this.versions = arrayToStringMap(getElectronVersions());
this.versions = arrayToStringMap(getAllElectronVersions());
this.updateDownloadedVersionState();
}
@@ -463,16 +398,16 @@ export class AppState {
const updatedVersions = { ...this.versions };
// Actually remove
if (release && release.source === VersionSource.local) {
if (release && release.source === ElectronVersionSource.local) {
delete updatedVersions[version];
const versionsAsArray = Object.keys(updatedVersions).map(
(k) => updatedVersions[k],
);
const versionsAsArray = Object
.keys(updatedVersions)
.map((k) => updatedVersions[k]);
saveLocalVersions(versionsAsArray);
} else {
await removeBinary(version);
await this.binaryManager.remove(version);
updatedVersions[version].state = VersionState.unknown;
}
@@ -491,25 +426,45 @@ export class AppState {
console.log(`State: Downloading Electron ${version}`);
const release = this.versions[version] || { state: '', source: '' };
const isLocal = release.source === VersionSource.local;
const isLocal = release.source === ElectronVersionSource.local;
const isReady = release.state === 'ready';
// Fetch new binaries, maybe?
if (!isLocal && !isReady) {
console.log(`State: Fetching v${version}`);
console.log(`State: Instructing BinaryManager to fetch v${version}`);
const updatedVersions = { ...this.versions };
updatedVersions[version] = updatedVersions[version] || {};
updatedVersions[version].state = VersionState.downloading;
this.versions = updatedVersions;
await setupBinary(this, version);
await this.binaryManager.setup(version);
this.updateDownloadedVersionState();
} else {
console.log(
`State: Version ${version} already downloaded, doing nothing.`,
);
console.log(`State: Version ${version} already downloaded, doing nothing.`);
}
}
/**
* Select a version of Node.js (and download fi necessary)
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async setNodeVersion(input: string) {
const version = normalizeVersion(input);
if (!this.nodeVersions[version]) {
console.warn(`State: Called setNodeVersion() with ${version}, which does not exist.`);
this.setVersion(this.nodeVersions[0].version);
return;
}
console.log(`State: Switching to Node ${version}`);
this.nodeVersion = version;
}
/**
* Select a version of Electron (and download it if necessary).
*
@@ -520,9 +475,7 @@ export class AppState {
const version = normalizeVersion(input);
if (!this.versions[version]) {
console.warn(
`State: Called setVersion() with ${version}, which does not exist.`,
);
console.warn(`State: Called setVersion() with ${version}, which does not exist.`);
this.setVersion(knownVersions[0].version);
return;
@@ -541,27 +494,17 @@ export class AppState {
// Update TypeScript definitions
const versionObject = this.versions[version];
if (versionObject.source === VersionSource.local) {
if (versionObject.source === ElectronVersionSource.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}`,
);
try {
this.localTypeWatcher = fs.watch(typePath!, async () => {
console.info(
`TypeDefs: Noticed file change at ${typePath}. Updating editor typedefs.`,
);
await updateEditorTypeDefinitions(versionObject);
});
} catch (err) {
console.info('TypeDefs: Unable to start watching.');
}
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.`,
);
console.info(`TypeDefs: Switched to downloaded version ${version}. Unwatching local typedefs.`);
this.localTypeWatcher.close();
this.localTypeWatcher = undefined;
}
@@ -578,23 +521,14 @@ export class AppState {
* @returns {Promise<void>}
*/
@action public async updateDownloadedVersionState(): Promise<void> {
const downloadedVersions = await this.binaryManager.getDownloadedVersions();
const updatedVersions = { ...this.versions };
// Keep state of currently downloading binaries first
const downloadingVersions = getDownloadingVersions(this);
(downloadingVersions || []).forEach((version) => {
if (updatedVersions[version]) {
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;
@@ -619,8 +553,7 @@ export class AppState {
* @param {(string | Buffer)} data
*/
@action public pushOutput(
data: string | Buffer,
options: OutputOptions = { isNotPre: false, bypassBuffer: true },
data: string | Buffer, options: OutputOptions = { isNotPre: false, bypassBuffer: true }
) {
let strData = data.toString();
const { isNotPre, bypassBuffer } = options;
@@ -630,6 +563,7 @@ export class AppState {
this.outputBuffer += strData;
strData = this.outputBuffer;
const parts = strData.split('\r\n');
for (let partIndex = 0; partIndex < parts.length; partIndex += 1) {
const part = parts[partIndex];
if (partIndex === parts.length - 1) {
@@ -649,7 +583,7 @@ export class AppState {
this.output.push({
timestamp: Date.now(),
text: strData.trim(),
isNotPre,
isNotPre
});
}
@@ -671,9 +605,7 @@ export class AppState {
*
* @param {EditorId} id
*/
@action public getAndRemoveEditorValueBackup(
id: EditorId,
): EditorBackup | null {
@action public getAndRemoveEditorValueBackup(id: EditorId): EditorBackup | null {
const value = this.closedPanels[id];
if (isEditorBackup(value)) {
@@ -684,42 +616,27 @@ export class AppState {
return null;
}
@action public async setVisibleMosaics(visible: Array<MosaicId>) {
@action public setVisibleMosaics(visible: Array<MosaicId>) {
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
for (const id of ALL_MOSAICS) {
if (!visible.includes(id) && currentlyVisible.includes(id)) {
this.closedPanels[id] = isEditorId(id) ? getEditorBackup(id) : true;
// if we have backup, remove active editor
delete window.ElectronFiddle.editors[id];
this.closedPanels[id] = isEditorId(id)
? getEditorBackup(id)
: true;
}
// Remove the backup for panels now. Editors will remove their
// backup once the data has been loaded.
if (
isPanelId(id) &&
visible.includes(id) &&
!currentlyVisible.includes(id)
) {
if (isPanelId(id) && visible.includes(id) && !currentlyVisible.includes(id)) {
delete this.closedPanels[id];
}
}
const updatedArrangement = createMosaicArrangement(visible);
console.log(
`State: Setting visible mosaic panels`,
visible,
updatedArrangement,
);
console.log(`State: Setting visible mosaic panels`, visible, updatedArrangement);
this.mosaicArrangement = updatedArrangement;
// after the mosaicArrangement loads, we want to wait for the Mosaic editors to
// mount to ensure that we can load content into the editors as soon as they're
// declared visible.
await waitForEditorsToMount(visible);
}
/**
@@ -742,15 +659,6 @@ export class AppState {
this.setVisibleMosaics([...currentlyVisible, id]);
}
/**
* Resets editor view to default layout
*
*
*/
@action public resetEditorLayout() {
this.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
}
/**
* Resets the view, optionally with certain view flags enabled.
*
@@ -799,19 +707,11 @@ export class AppState {
* @param {string} key
* @param {(string | number | object)} [value]
*/
private save(
key: string,
value?:
| string
| number
| Array<any>
| Record<string, unknown>
| null
| boolean,
) {
private save(key: string, value?: string | number | object | null | boolean) {
if (value) {
const _value =
typeof value === 'object' ? JSON.stringify(value) : value.toString();
const _value = typeof value === 'object'
? JSON.stringify(value)
: value.toString();
localStorage.setItem(key, _value);
} else {
+3 -13
View File
@@ -2,13 +2,7 @@ import * as fsExtraType from 'fs-extra';
import * as pathType from 'path';
import { EditorValues } from '../interfaces';
import {
INDEX_HTML_NAME,
MAIN_JS_NAME,
PRELOAD_JS_NAME,
RENDERER_JS_NAME,
STYLES_CSS_NAME,
} from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { fancyImport } from '../utils/import';
/**
@@ -34,10 +28,7 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
if (fs.existsSync(templatesPath)) {
const contents = fs.readdirSync(templatesPath);
console.log(
`getTemplateValues(): ${templatesPath} contents:`,
contents,
);
console.log(`getTemplateValues(): ${templatesPath} contents:`, contents);
} else {
console.log(`getTemplateValues(): ${templatesPath} does not exist`);
}
@@ -50,7 +41,6 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
renderer: await getFile(RENDERER_JS_NAME),
main: await getFile(MAIN_JS_NAME),
html: await getFile(INDEX_HTML_NAME),
preload: await getFile(PRELOAD_JS_NAME),
css: await getFile(STYLES_CSS_NAME),
preload: await getFile(PRELOAD_JS_NAME)
};
}
+10 -8
View File
@@ -53,16 +53,17 @@ export const defaultDark: LoadedFiddleTheme = {
'text-color-2': '#1e2527',
'text-color-3': '#dcdcdc',
'error-color': '#df3434',
'fonts-common': `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
// tslint:disable-next-line:max-line-length
'fonts-common': `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`
},
editor: {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#2f3241',
},
},
'editor.background': '#2f3241'
}
}
};
export const defaultLight: LoadedFiddleTheme = {
@@ -85,17 +86,18 @@ export const defaultLight: LoadedFiddleTheme = {
'text-color-2': '#1e2527',
'text-color-3': '#0e0e0e',
'error-color': '#df3434',
'fonts-common': `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
// tslint:disable-next-line:max-line-length
'fonts-common': `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`
},
editor: {
base: 'vs',
inherit: true,
rules: [],
colors: {},
},
colors: {}
}
};
export enum DefaultThemes {
DARK = 'defaultDark',
LIGHT = 'defaultLight',
LIGHT = 'defaultLight'
}
+11 -23
View File
@@ -4,13 +4,7 @@ import * as path from 'path';
import { fancyImport } from '../utils/import';
import { CONFIG_PATH } from './constants';
import {
defaultDark,
defaultLight,
DefaultThemes,
FiddleTheme,
LoadedFiddleTheme,
} from './themes-defaults';
import { defaultDark, defaultLight, DefaultThemes, FiddleTheme, LoadedFiddleTheme } from './themes-defaults';
export const THEMES_PATH = path.join(CONFIG_PATH, 'themes');
@@ -21,12 +15,10 @@ export const THEMES_PATH = path.join(CONFIG_PATH, 'themes');
* @param {LoadedFiddleTheme} [theme]
*/
export async function activateTheme(
monaco?: typeof MonacoType,
theme?: LoadedFiddleTheme,
name?: string | null,
monaco?: typeof MonacoType, theme?: LoadedFiddleTheme, name?: string | null
) {
const _monaco = monaco || window.ElectronFiddle.app.monaco;
const _theme = theme || (await getTheme(name));
const _theme = theme || await getTheme(name);
if (!_monaco || !_monaco.editor) return;
@@ -34,6 +26,7 @@ export async function activateTheme(
_monaco.editor.setTheme('main');
}
/**
* Read in a theme file.
*
@@ -41,9 +34,7 @@ export async function activateTheme(
* @param {string} [name]
* @returns {Promise<FiddleTheme>}
*/
export async function readThemeFile(
name?: string,
): Promise<LoadedFiddleTheme | null> {
export async function readThemeFile(name?: string): Promise<LoadedFiddleTheme | null> {
if (!name || name === DefaultThemes.DARK) return defaultDark as any;
if (name === DefaultThemes.LIGHT) return defaultLight as any;
@@ -56,7 +47,7 @@ export async function readThemeFile(
return {
...theme,
name: theme.name || name.replace('.json', ''),
file,
file
};
} catch (error) {
console.warn(`Themes: Loading theme ${name} failed`, error);
@@ -64,6 +55,7 @@ export async function readThemeFile(
}
}
/**
* Reads and then returns all available themes.
*
@@ -73,7 +65,7 @@ export async function getAvailableThemes(): Promise<Array<LoadedFiddleTheme>> {
const fs = await fancyImport<typeof fsType>('fs-extra');
const themes: Array<LoadedFiddleTheme> = [
defaultDark as any,
defaultLight as any,
defaultLight as any
];
if (!fs.existsSync(THEMES_PATH)) {
@@ -105,11 +97,9 @@ export async function getAvailableThemes(): Promise<Array<LoadedFiddleTheme>> {
* @param {string} [name]
* @returns {Promise<FiddleTheme>}
*/
export async function getTheme(
name?: string | null,
): Promise<LoadedFiddleTheme> {
export async function getTheme(name?: string | null): Promise<LoadedFiddleTheme> {
console.log(`Themes: getTheme() loading ${name || 'default'}`);
const theme = (await readThemeFile(name || undefined)) || defaultDark;
const theme = await readThemeFile(name || undefined) || defaultDark;
return { ...theme, css: await getCssStringForTheme(theme) };
}
@@ -120,9 +110,7 @@ export async function getTheme(
* @param {FiddleTheme} theme
* @returns {string}
*/
export async function getCssStringForTheme(
theme: FiddleTheme,
): Promise<string> {
export async function getCssStringForTheme(theme: FiddleTheme): Promise<string> {
let cssContent = '';
Object.keys(theme.common).forEach((key) => {
+49 -38
View File
@@ -1,13 +1,13 @@
import {
remote,
TouchBarButtonConstructorOptions,
TouchBarScrubberConstructorOptions,
TouchBarScrubberConstructorOptions
} from 'electron';
import { autorun } from 'mobx';
import { RunnableVersion, VersionState } from '../interfaces';
import { ElectronVersion, VersionState } from '../interfaces';
import { getNiceGreeting } from '../utils/nice-greeting';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import { sortedElectronMap } from '../utils/sorted-map';
import { AppState } from './state';
const { TouchBar } = remote;
@@ -15,20 +15,22 @@ const {
TouchBarButton,
TouchBarScrubber,
TouchBarSpacer,
TouchBarLabel,
TouchBarLabel
} = TouchBar;
/**
* Helper method: Returns an icon (emoji, lol) for an Electron
* version.
*
* @param {Partial<RunnableVersion>} { state }
* @param {Partial<ElectronVersion>} { state }
* @returns {string}
*/
export function getItemIcon(
{ state }: Partial<RunnableVersion> = { state: VersionState.unknown },
{ state }: Partial<ElectronVersion> = { state: VersionState.unknown }
) {
return state === 'ready' ? '💾' : state === 'downloading' ? '⏬' : '☁';
return state === 'ready'
? '💾'
: state === 'downloading' ? '⏬' : '☁';
}
export class TouchBarManager {
@@ -38,28 +40,31 @@ export class TouchBarManager {
public touchBar: Electron.TouchBar;
public versionSelector: Electron.TouchBarScrubber;
public versionSelectorBtn: Electron.TouchBarButton;
public selectedVersion = '';
public selectedVersion: string = '';
public versionSelectorSelectBtn: Electron.TouchBarButton;
// Lol have mercy, TS
public items: Array<Electron.TouchBarButton
| Electron.TouchBarColorPicker
| Electron.TouchBarGroup
| Electron.TouchBarLabel
| Electron.TouchBarPopover
| Electron.TouchBarScrubber
| Electron.TouchBarSegmentedControl
| Electron.TouchBarSlider
| Electron.TouchBarSpacer>
| undefined;
constructor(public readonly appState: AppState) {
this.selectVersion = this.selectVersion.bind(this);
this.updateStartStopBtn = this.updateStartStopBtn.bind(this);
this.updateVersionSelectorItems = this.updateVersionSelectorItems.bind(
this,
);
this.updateVersionSelectorItems = this.updateVersionSelectorItems.bind(this);
this.updateVersionButton = this.updateVersionButton.bind(this);
this.startStopBtn = new TouchBarButton(this.getStartStopButtonOptions());
this.consoleBtn = new TouchBarButton(this.getConsoleButtonOptions());
this.versionSelectorBtn = new TouchBarButton(
this.getVersionButtonOptions(),
);
this.versionSelectorSelectBtn = new TouchBarButton(
this.getVersionSelectorSelectButtonOptions(),
);
this.versionSelector = new TouchBarScrubber(
this.getVersionSelectorOptions(),
);
this.versionSelectorBtn = new TouchBarButton(this.getVersionButtonOptions());
this.versionSelectorSelectBtn = new TouchBarButton(this.getVersionSelectorSelectButtonOptions());
this.versionSelector = new TouchBarScrubber(this.getVersionSelectorOptions());
this.setupTouchBar();
}
@@ -120,9 +125,8 @@ export class TouchBarManager {
*/
public setTouchBar(options: Partial<Electron.TouchBarConstructorOptions>) {
try {
this.touchBar = new TouchBar(
options as Electron.TouchBarConstructorOptions,
);
this.items = options.items;
this.touchBar = new TouchBar(options as Electron.TouchBarConstructorOptions);
this.browserWindow = this.browserWindow || remote.getCurrentWindow();
this.browserWindow.setTouchBar(this.touchBar);
} catch (error) {
@@ -138,8 +142,8 @@ export class TouchBarManager {
items: [
new TouchBarSpacer({ size: 'flexible' }),
new TouchBarLabel({ label: getNiceGreeting() }),
new TouchBarSpacer({ size: 'flexible' }),
],
new TouchBarSpacer({ size: 'flexible' })
]
});
}
@@ -153,8 +157,8 @@ export class TouchBarManager {
this.consoleBtn,
new TouchBarSpacer({ size: 'flexible' }),
this.versionSelectorBtn,
new TouchBarSpacer({ size: 'flexible' }),
],
new TouchBarSpacer({ size: 'flexible' })
]
});
}
@@ -163,8 +167,13 @@ export class TouchBarManager {
*/
public setVersionSelectorItems() {
this.setTouchBar({
items: [this.versionSelector, this.versionSelectorSelectBtn],
escapeItem: new TouchBarButton(this.getVersionSelectorEscButtonOptions()),
items: [
this.versionSelector,
this.versionSelectorSelectBtn,
],
escapeItem: new TouchBarButton(
this.getVersionSelectorEscButtonOptions()
)
});
}
@@ -177,7 +186,7 @@ export class TouchBarManager {
label: `Back`,
click: () => {
this.setDefaultItems();
},
}
};
}
@@ -194,7 +203,7 @@ export class TouchBarManager {
}
this.setDefaultItems();
},
}
};
}
@@ -205,12 +214,13 @@ export class TouchBarManager {
* @returns {TouchBarButtonConstructorOptions}
*/
public getStartStopButtonOptions(): TouchBarButtonConstructorOptions {
const label = this.appState.isRunning ? '🛑 Stop' : '🚀 Run';
const label = this.appState.isRunning
? '🛑 Stop'
: '🚀 Run';
const click = () =>
this.appState.isRunning
? window.ElectronFiddle.app.runner.stop()
: window.ElectronFiddle.app.runner.run();
const click = () => this.appState.isRunning
? window.ElectronFiddle.app.runner.stop()
: window.ElectronFiddle.app.runner.run();
return { label, click };
}
@@ -245,7 +255,8 @@ export class TouchBarManager {
*
* @returns {TouchBarScrubberConstructorOptions}
*/
public getVersionSelectorOptions(): TouchBarScrubberConstructorOptions {
public getVersionSelectorOptions(
): TouchBarScrubberConstructorOptions {
return {
select: this.selectVersion,
highlight: this.selectVersion,
@@ -254,7 +265,7 @@ export class TouchBarManager {
overlayStyle: 'outline',
mode: 'free',
continuous: true,
showArrowButtons: false,
showArrowButtons: false
};
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { Files } from '../../interfaces';
* @returns {Promise<Files>}
*/
export async function dotfilesTransform(files: Files): Promise<Files> {
files.set('.gitignore', 'node_modules\nout');
files.set('.gitignore', 'node_modules\nout');
return files;
}
+11 -10
View File
@@ -15,12 +15,11 @@ export async function forgeTransform(files: Files): Promise<Files> {
// devDependencies
parsed.devDependencies = parsed.devDependencies || {};
parsed.devDependencies['@electron-forge/cli'] = '6.0.0-beta.52';
parsed.devDependencies['@electron-forge/maker-deb'] = '6.0.0-beta.52';
parsed.devDependencies['@electron-forge/maker-rpm'] = '6.0.0-beta.52';
parsed.devDependencies['@electron-forge/maker-squirrel'] =
'6.0.0-beta.52';
parsed.devDependencies['@electron-forge/maker-zip'] = '6.0.0-beta.52';
parsed.devDependencies['@electron-forge/cli'] = '6.0.0-beta.34';
parsed.devDependencies['@electron-forge/maker-deb'] = '6.0.0-beta.34';
parsed.devDependencies['@electron-forge/maker-rpm'] = '6.0.0-beta.34';
parsed.devDependencies['@electron-forge/maker-squirrel'] = '6.0.0-beta.34';
parsed.devDependencies['@electron-forge/maker-zip'] = '6.0.0-beta.34';
// Scripts
parsed.scripts = parsed.scripts || {};
@@ -42,16 +41,18 @@ export async function forgeTransform(files: Files): Promise<Files> {
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
platforms: [
'darwin'
]
},
{
name: '@electron-forge/maker-deb',
config: {},
config: {}
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
config: {}
}
];
files.set(PACKAGE_NAME, JSON.stringify(parsed, undefined, 2));
+118 -92
View File
@@ -1,45 +1,32 @@
import semver from 'semver';
import {
RunnableVersion,
Version,
VersionSource,
VersionState,
} from '../interfaces';
import { ElectronVersion, ElectronVersionSource, NodeVersion, NpmVersion, VersionState } from '../interfaces';
import { normalizeVersion } from '../utils/normalize-version';
export const enum ElectronReleaseChannel {
stable = 'Stable',
beta = 'Beta',
nightly = 'Nightly',
unsupported = 'Unsupported',
unsupported = 'Unsupported'
}
/**
* Returns a sensible default version string.
*
* @param {Array<RunnableVersion>} knownVersions
* @param {Array<ElectronVersion>} knownVersions
* @returns {string}
*/
export function getDefaultVersion(
knownVersions: Array<RunnableVersion> = [],
knownVersions: Array<ElectronVersion> = []
): string {
const ls = localStorage.getItem('version');
if (
ls &&
knownVersions &&
knownVersions.find(({ version }) => version === ls)
) {
if (ls && knownVersions && knownVersions.find(({ version }) => version === ls)) {
return ls;
}
// Self-heal: Version not formated correctly
const normalized = ls && normalizeVersion(ls);
if (normalized) {
if (
knownVersions &&
knownVersions.find(({ version }) => version === normalized)
) {
if (knownVersions && knownVersions.find(({ version }) => version === normalized)) {
return normalized;
}
}
@@ -58,13 +45,14 @@ export function getDefaultVersion(
* Return the release channel for a given input
* version.
*
* @param {Version | string} input
* @param {NpmVersion | string} input
* @returns {ElectronReleaseChannel}
*/
export function getReleaseChannel(
input: Version | string,
input: NpmVersion | string
): ElectronReleaseChannel {
const tag = typeof input === 'string' ? input : input.version || '';
const tag = (typeof input === 'string') ? input : (input.version || '');
if (tag.includes('beta')) {
return ElectronReleaseChannel.beta;
@@ -84,32 +72,29 @@ export function getReleaseChannel(
export const enum VersionKeys {
local = 'local-electron-versions',
known = 'known-electron-versions',
known = 'known-electron-versions'
}
/**
* Retrieve Electron versions from localStorage.
*
* @param {VersionKeys} key
* @param {() => Array<Version>} fallbackMethod
* @returns {Array<Version>}
* @param {() => Array<NpmVersion>} fallbackMethod
* @returns {Array<NpmVersion>}
*/
function getVersions(
key: VersionKeys,
fallbackMethod: () => Array<Version>,
): Array<Version> {
function getElectronVersions(
key: VersionKeys, fallbackMethod: () => Array<NpmVersion>
): Array<NpmVersion> {
const fromLs = window.localStorage.getItem(key);
if (fromLs) {
try {
let result: Array<Version> = JSON.parse(fromLs);
let result: Array<NpmVersion> = JSON.parse(fromLs);
if (!isExpectedFormat(result)) {
// Known versions can just be downloaded again.
if (key === VersionKeys.known) {
throw new Error(
`Electron versions in LS does not match expected format`,
);
throw new Error(`Electron versions in LS does not match expected format`);
}
// Local versions are a bit more tricky and might be in an old format (pre 0.5)
@@ -119,9 +104,7 @@ function getVersions(
return result;
} catch (error) {
console.warn(
`Parsing local Electron versions failed, returning fallback method.`,
);
console.warn(`Parsing local Electron versions failed, returning fallback method.`);
}
}
@@ -132,9 +115,9 @@ function getVersions(
* Save an array of GitHubVersions to localStorage.
*
* @param {VersionKeys} key
* @param {Array<Version} versions
* @param {Array<NpmVersion} versions
*/
function saveVersions(key: VersionKeys, versions: Array<Version>) {
function saveElectronVersions(key: VersionKeys, versions: Array<NpmVersion>) {
const stringified = JSON.stringify(versions);
window.localStorage.setItem(key, stringified);
}
@@ -142,22 +125,22 @@ function saveVersions(key: VersionKeys, versions: Array<Version>) {
/**
* Return both known as well as local versions.
*
* @returns {Array<Version>}
* @returns {Array<NpmVersion>}
*/
export function getElectronVersions(): Array<RunnableVersion> {
const known: Array<RunnableVersion> = getKnownVersions().map((version) => {
export function getAllElectronVersions(): Array<ElectronVersion> {
const known: Array<ElectronVersion> = getKnownVersions().map((version) => {
return {
...version,
source: VersionSource.remote,
state: VersionState.unknown,
source: ElectronVersionSource.remote,
state: VersionState.unknown
};
});
const local: Array<RunnableVersion> = getLocalVersions().map((version) => {
const local: Array<ElectronVersion> = getLocalVersions().map((version) => {
return {
...version,
source: VersionSource.local,
state: VersionState.ready,
source: ElectronVersionSource.local,
state: VersionState.ready
};
});
@@ -167,10 +150,10 @@ export function getElectronVersions(): Array<RunnableVersion> {
/**
* Add a version to the local versions
*
* @param {Version} input
* @returns {Array<Version>}
* @param {NpmVersion} input
* @returns {Array<NpmVersion>}
*/
export function addLocalVersion(input: Version): Array<Version> {
export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
const versions = getLocalVersions();
if (!versions.find((v) => v.localPath === input.localPath)) {
@@ -185,10 +168,10 @@ export function addLocalVersion(input: Version): Array<Version> {
/**
* Retrieves local Electron versions, configured by the user.
*
* @returns {Array<Version>}
* @returns {Array<NpmVersion>}
*/
export function getLocalVersions(): Array<Version> {
const versions = getVersions(VersionKeys.local, () => []);
export function getLocalVersions(): Array<NpmVersion> {
const versions = getElectronVersions(VersionKeys.local, () => []);
return versions;
}
@@ -196,39 +179,37 @@ export function getLocalVersions(): Array<Version> {
/**
* Saves local versions to localStorage.
*
* @param {Array<Version>} versions
* @param {Array<NpmVersion>} versions
*/
export function saveLocalVersions(versions: Array<Version | RunnableVersion>) {
export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>) {
const filteredVersions = versions.filter((v) => {
if (isElectronVersion(v)) {
return v.source === VersionSource.local;
return v.source === ElectronVersionSource.local;
}
return true;
});
return saveVersions(VersionKeys.local, filteredVersions);
return saveElectronVersions(VersionKeys.local, filteredVersions);
}
/**
* Retrieves our best guess regarding the latest Electron versions. Tries to
* fetch them from localStorage, then from a static releases.json file.
*
* @returns {Array<Version>}
* @returns {Array<NpmVersion>}
*/
export function getKnownVersions(): Array<Version> {
return getVersions(VersionKeys.known, () =>
require('../../static/releases.json'),
);
export function getKnownVersions(): Array<NpmVersion> {
return getElectronVersions(VersionKeys.known, () => require('../../static/releases.json'));
}
/**
* Saves known versions to localStorage.
*
* @param {Array<Version>} versions
* @param {Array<NpmVersion>} versions
*/
export function saveKnownVersions(versions: Array<Version>) {
return saveVersions(VersionKeys.known, versions);
export function saveKnownVersions(versions: Array<NpmVersion>) {
return saveElectronVersions(VersionKeys.known, versions);
}
/**
@@ -236,40 +217,85 @@ export function saveKnownVersions(versions: Array<Version>) {
* saved after.
*
* @export
* @returns {Promise<Array<RunnableVersion>>}
* @returns {Promise<Array<ElectronVersion>>}
*/
export async function getUpdatedElectronVersions(): Promise<
Array<RunnableVersion>
> {
export async function getUpdatedElectronVersions(
): Promise<Array<ElectronVersion>> {
try {
await fetchVersions();
await fetchElectronVersions();
} catch (error) {
console.warn(`Versions: Failed to fetch versions`, { error });
console.warn(`Versions: Failed to fetch Electron versions`, { error });
}
return getElectronVersions();
return getAllElectronVersions();
}
export async function getUpdatedNodeVersions(): Promise<Array<NodeVersion>> {
try {
return await fetchNodeVersions();
} catch (error) {
console.warn(`Versions: Failed to fetch Node.js versions`, { error });
}
return [];
}
/**
* Fetch the latest known versions directly from npm.
* Fetch the latest known Node.js versions directly from Node.js.
*
* @returns {Promise<Array<Version>>}
* @returns {Promise<Array<NpmVersion>>}
*
* let entry = {
* "headers": 1,
* "linux-arm64": 2,
* "linux-x64": 6,
* "osx-x64-tar": 8,
* "win-x64-zip": 14,
* "win-x86-zip": 18
* }
*
* nodejs.org/dist/${version}/node-${version}-${files[entry]}.file-ext
* ^ for windows, take the plat and extend with the file extension in the identifier above
*/
export async function fetchVersions() {
const response = await window.fetch(
'https://unpkg.com/electron-releases/lite.json',
);
const data = await response.json();
export async function fetchNodeVersions() {
const channels = [
`https://nodejs.org/dist/index.json`
];
// pre-0.24.0 versions were technically 'atom-shell' and cannot
// be downloaded with @electron/get
const MIN_DOWNLOAD_VERSION = '0.24.0';
let output: Array<NodeVersion> = [];
const output = data
.map(({ version }: any) => ({ version }))
.filter(({ version }: any) => semver.gte(version, MIN_DOWNLOAD_VERSION));
for (const channelUrl of channels) {
const response = await window.fetch(channelUrl);
const versions = await response.json();
if (output?.length > 0 && isExpectedFormat(output)) {
output = versions.map((input: any) => ({
version: normalizeVersion(input.version)
}));
}
return output;
}
/**
* Fetch the latest known Electron versions directly from npm.
*
* @returns {Promise<Array<NpmVersion>>}
*/
export async function fetchElectronVersions() {
const channels = [
`https://registry.npmjs.org/electron`, // stable, beta
`https://registry.npmjs.org/electron-nightly` // nightly
];
const output: Array<NpmVersion> = [];
for (const channelUrl of channels) {
const response = await window.fetch(channelUrl);
const data = await response.json();
const versions: Record<string, any> = data.versions;
output.push(...Object.keys(versions).map((version) => ({ version })));
}
if (output && output.length > 0 && isExpectedFormat(output)) {
console.log(`Fetched new Electron versions (Count: ${output.length})`);
saveKnownVersions(output);
}
@@ -291,9 +317,9 @@ export function isExpectedFormat(input: Array<any>): boolean {
* Migrates old versions, if necessary
*
* @param {Array<any>} input
* @returns {Array<Version>}
* @returns {Array<NpmVersion>}
*/
export function migrateVersions(input: Array<any> = []): Array<Version> {
export function migrateVersions(input: Array<any> = []): Array<NpmVersion> {
return input
.filter((item) => !!item)
.map((item) => {
@@ -304,14 +330,14 @@ export function migrateVersions(input: Array<any> = []): Array<Version> {
return {
version: tag_name,
name,
localPath: url,
localPath: url
};
})
.filter((item) => !!item) as Array<Version>;
.filter((item) => !!item) as Array<NpmVersion>;
}
export function isElectronVersion(
input: Version | RunnableVersion,
): input is RunnableVersion {
return (input as RunnableVersion).source !== undefined;
input: NpmVersion | ElectronVersion
): input is ElectronVersion {
return (input as ElectronVersion).source !== undefined;
}
-10
View File
@@ -1,10 +0,0 @@
import * as Sentry from '@sentry/electron';
import { isDevMode } from './utils/devmode';
export function initSentry() {
if (!(global as any).__JEST__ && !isDevMode()) {
Sentry.init({
dsn: 'https://966a5b01ac8d4941b81e4ebd0ab4c991@sentry.io/1882540',
});
}
}
-1
View File
@@ -2,5 +2,4 @@ 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';
+2 -2
View File
@@ -29,6 +29,6 @@ export const SHOW_ME_TEMPLATES: Templates = {
TouchBar: 'TouchBar',
Tray: 'Tray',
WebContents: 'WebContents',
WebFrame: 'WebFrame',
},
WebFrame: 'WebFrame'
}
};
+16
View File
@@ -0,0 +1,16 @@
import * as path from 'path';
import { remote } from 'electron';
let appData = '';
/**
* Returns an appData path for a given input
*
* @param input {string}
* @returns {string}
*/
export function getAppDataDir(input: string): string {
appData = appData || remote.app.getPath('appData');
return path.join(appData, input);
}
+6 -6
View File
@@ -1,16 +1,16 @@
import { RunnableVersion } from '../interfaces';
import { Version } from '../interfaces';
import { normalizeVersion } from './normalize-version';
/**
* Takes an array of GitHub releases and returns a StringMap of
* Electron releases.
*
* @param {Array<RunnableVersion>} input
* @returns {Record<string, RunnableVersion>}
* @param {Array<ElectronVersion>} input
* @returns {Record<string, ElectronVersion>}
*/
export function arrayToStringMap(
input: Array<RunnableVersion>,
): Record<string, RunnableVersion> {
export function arrayToStringMap<T extends Version>(
input: Array<T>
): Record<string, T> {
const output = {};
input.forEach((version) => {

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