ing
This commit is contained in:
parent
648efb7a7a
commit
a9f514b9fb
21
@overflow/core/menu-item.ts
Normal file
21
@overflow/core/menu-item.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export interface IMenuItem {
|
||||||
|
/** The user-facing label. */
|
||||||
|
readonly label?: string;
|
||||||
|
|
||||||
|
/** The action to invoke when the user selects the item. */
|
||||||
|
readonly action?: () => void;
|
||||||
|
|
||||||
|
/** The type of item. */
|
||||||
|
readonly type?: 'separator';
|
||||||
|
|
||||||
|
/** Is the menu item enabled? Defaults to true. */
|
||||||
|
readonly enabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The predefined behavior of the menu item.
|
||||||
|
*
|
||||||
|
* When specified the click property will be ignored.
|
||||||
|
* See https://electronjs.org/docs/api/menu-item#roles
|
||||||
|
*/
|
||||||
|
readonly role?: string;
|
||||||
|
}
|
145
@overflow/core/parse-app-url.ts
Normal file
145
@overflow/core/parse-app-url.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import * as URL from 'url';
|
||||||
|
|
||||||
|
export interface IOAuthAction {
|
||||||
|
readonly name: 'oauth';
|
||||||
|
readonly code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOpenRepositoryFromURLAction {
|
||||||
|
readonly name: 'open-repository-from-url';
|
||||||
|
|
||||||
|
/** the remote repository location associated with the "Open in Desktop" action */
|
||||||
|
readonly url: string;
|
||||||
|
|
||||||
|
/** the optional branch name which should be checked out. use the default branch otherwise. */
|
||||||
|
readonly branch: string | null;
|
||||||
|
|
||||||
|
/** the pull request number, if pull request originates from a fork of the repository */
|
||||||
|
readonly pr: string | null;
|
||||||
|
|
||||||
|
/** the file to open after cloning the repository */
|
||||||
|
readonly filepath: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOpenRepositoryFromPathAction {
|
||||||
|
readonly name: 'open-repository-from-path';
|
||||||
|
|
||||||
|
/** The local path to open. */
|
||||||
|
readonly path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUnknownAction {
|
||||||
|
readonly name: 'unknown';
|
||||||
|
readonly url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type URLActionType =
|
||||||
|
| IOAuthAction
|
||||||
|
| IOpenRepositoryFromURLAction
|
||||||
|
| IOpenRepositoryFromPathAction
|
||||||
|
| IUnknownAction;
|
||||||
|
|
||||||
|
// eslint-disable-next-line typescript/interface-name-prefix
|
||||||
|
interface ParsedUrlQueryWithUndefined {
|
||||||
|
// `undefined` is added here to ensure we handle the missing querystring key
|
||||||
|
// See https://github.com/Microsoft/TypeScript/issues/13778 for discussion about
|
||||||
|
// why this isn't supported natively in TypeScript
|
||||||
|
[key: string]: string | string[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the URL to find a given key in the querystring text.
|
||||||
|
*
|
||||||
|
* @param url The source URL containing querystring key-value pairs
|
||||||
|
* @param key The key to look for in the querystring
|
||||||
|
*/
|
||||||
|
function getQueryStringValue(
|
||||||
|
query: ParsedUrlQueryWithUndefined,
|
||||||
|
key: string
|
||||||
|
): string | null {
|
||||||
|
const value = query[key];
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAppURL(url: string): URLActionType {
|
||||||
|
const parsedURL = URL.parse(url, true);
|
||||||
|
const hostname = parsedURL.hostname;
|
||||||
|
const unknown: IUnknownAction = { name: 'unknown', url };
|
||||||
|
if (!hostname) {
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = parsedURL.query;
|
||||||
|
|
||||||
|
const actionName = hostname.toLowerCase();
|
||||||
|
if (actionName === 'oauth') {
|
||||||
|
const code = getQueryStringValue(query, 'code');
|
||||||
|
if (code != null) {
|
||||||
|
return { name: 'oauth', code };
|
||||||
|
} else {
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we require something resembling a URL first
|
||||||
|
// - bail out if it's not defined
|
||||||
|
// - bail out if you only have `/`
|
||||||
|
const pathName = parsedURL.pathname;
|
||||||
|
if (!pathName || pathName.length <= 1) {
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim the trailing / from the URL
|
||||||
|
const parsedPath = pathName.substr(1);
|
||||||
|
|
||||||
|
if (actionName === 'openrepo') {
|
||||||
|
const probablyAURL = parsedPath;
|
||||||
|
|
||||||
|
// suffix the remote URL with `.git`, for backwards compatibility
|
||||||
|
const _url = `${probablyAURL}.git`;
|
||||||
|
|
||||||
|
const pr = getQueryStringValue(query, 'pr');
|
||||||
|
const branch = getQueryStringValue(query, 'branch');
|
||||||
|
const filepath = getQueryStringValue(query, 'filepath');
|
||||||
|
|
||||||
|
if (pr != null) {
|
||||||
|
if (!/^\d+$/.test(pr)) {
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we also expect the branch for a forked PR to be a given ref format
|
||||||
|
if (branch != null && !/^pr\/\d+$/.test(branch)) {
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (branch != null && testForInvalidChars(branch)) {
|
||||||
|
// return unknown;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'open-repository-from-url',
|
||||||
|
url: _url,
|
||||||
|
branch,
|
||||||
|
pr,
|
||||||
|
filepath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionName === 'openlocalrepo') {
|
||||||
|
return {
|
||||||
|
name: 'open-repository-from-path',
|
||||||
|
path: decodeURIComponent(parsedPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return unknown;
|
||||||
|
}
|
12
@overflow/core/path.ts
Normal file
12
@overflow/core/path.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import * as Path from 'path';
|
||||||
|
import fileUrl = require('file-url');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and encode the path information into a URL.
|
||||||
|
*
|
||||||
|
* @param pathSegments array of path segments to resolve
|
||||||
|
*/
|
||||||
|
export function encodePathAsUrl(...pathSegments: string[]): string {
|
||||||
|
const path = Path.resolve(...pathSegments);
|
||||||
|
return fileUrl(path);
|
||||||
|
}
|
90
@overflow/core/process/win32.ts
Normal file
90
@overflow/core/process/win32.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { spawn as spawnInternal } from 'child_process';
|
||||||
|
import * as Path from 'path';
|
||||||
|
|
||||||
|
/** Get the path segments in the user's `Path`. */
|
||||||
|
export async function getPathSegments(): Promise<ReadonlyArray<string>> {
|
||||||
|
let powershellPath: string;
|
||||||
|
const systemRoot = process.env.SystemRoot;
|
||||||
|
if (systemRoot != null) {
|
||||||
|
const system32Path = Path.join(systemRoot, 'System32');
|
||||||
|
powershellPath = Path.join(
|
||||||
|
system32Path,
|
||||||
|
'WindowsPowerShell',
|
||||||
|
'v1.0',
|
||||||
|
'powershell.exe'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
powershellPath = 'powershell.exe';
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
'-noprofile',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'RemoteSigned',
|
||||||
|
'-command',
|
||||||
|
// Set encoding and execute the command, capture the output, and return it
|
||||||
|
// via .NET's console in order to have consistent UTF-8 encoding.
|
||||||
|
// See http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
|
||||||
|
// to address https://github.com/atom/atom/issues/5063
|
||||||
|
`
|
||||||
|
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
|
||||||
|
$output=[environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
[Console]::WriteLine($output)
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const stdout = await spawn(powershellPath, args);
|
||||||
|
const pathOutput = stdout.replace(/^\s+|\s+$/g, '');
|
||||||
|
return pathOutput.split(/;+/).filter(segment => segment.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the user's `Path`. */
|
||||||
|
export async function setPathSegments(
|
||||||
|
paths: ReadonlyArray<string>
|
||||||
|
): Promise<void> {
|
||||||
|
let setxPath: string;
|
||||||
|
const systemRoot = process.env['SystemRoot'];
|
||||||
|
if (systemRoot) {
|
||||||
|
const system32Path = Path.join(systemRoot, 'System32');
|
||||||
|
setxPath = Path.join(system32Path, 'setx.exe');
|
||||||
|
} else {
|
||||||
|
setxPath = 'setx.exe';
|
||||||
|
}
|
||||||
|
|
||||||
|
await spawn(setxPath, ['Path', paths.join(';')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spawn a command with arguments and capture its output. */
|
||||||
|
export function spawn(
|
||||||
|
command: string,
|
||||||
|
args: ReadonlyArray<string>
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const child = spawnInternal(command, args as string[]);
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
let stdout = '';
|
||||||
|
child.stdout.on('data', data => {
|
||||||
|
stdout += data;
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', code => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(stdout);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Command "${command} ${args}" failed: "${stdout}"`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err: Error) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is necessary if using Powershell 2 on Windows 7 to get the events
|
||||||
|
// to raise.
|
||||||
|
// See http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
|
||||||
|
child.stdin.end();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
167
@overflow/core/shell.ts
Normal file
167
@overflow/core/shell.ts
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/* eslint-disable no-sync */
|
||||||
|
|
||||||
|
import * as ChildProcess from 'child_process';
|
||||||
|
import * as os from 'os';
|
||||||
|
|
||||||
|
interface IndexLookup {
|
||||||
|
[propName: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names of any env vars that we shouldn't copy from the shell environment.
|
||||||
|
*/
|
||||||
|
const BlacklistedNames = new Set(['LOCAL_GIT_DIRECTORY']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspect whether the current process needs to be patched to get important
|
||||||
|
* environment variables for Desktop to work and integrate with other tools
|
||||||
|
* the user may invoke as part of their workflow.
|
||||||
|
*
|
||||||
|
* This is only applied to macOS installations due to how the application
|
||||||
|
* is launched.
|
||||||
|
*
|
||||||
|
* @param process The process to inspect.
|
||||||
|
*/
|
||||||
|
export function shellNeedsPatching(process: NodeJS.Process): boolean {
|
||||||
|
return __DARWIN__ && !process.env.PWD;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShellResult {
|
||||||
|
stdout: string;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a dump of the user's configured shell environment.
|
||||||
|
*
|
||||||
|
* @returns the output of the `env` command or `null` if there was an error.
|
||||||
|
*/
|
||||||
|
async function getRawShellEnv(): Promise<string | null> {
|
||||||
|
const shell = getUserShell();
|
||||||
|
|
||||||
|
const promise = new Promise<ShellResult>(resolve => {
|
||||||
|
let child: ChildProcess.ChildProcess | null = null;
|
||||||
|
let _error: Error | null = null;
|
||||||
|
let _stdout = '';
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
// ensure we clean up eventually, in case things go bad
|
||||||
|
const cleanup = () => {
|
||||||
|
if (!done && child) {
|
||||||
|
child.kill();
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.once('exit', cleanup);
|
||||||
|
setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
detached: true,
|
||||||
|
stdio: ['ignore', 'pipe', process.stderr],
|
||||||
|
};
|
||||||
|
|
||||||
|
child = ChildProcess.spawn(shell, ['-ilc', 'command env'], options);
|
||||||
|
|
||||||
|
const buffers: Array<Buffer> = [];
|
||||||
|
|
||||||
|
child.on('error', (e: Error) => {
|
||||||
|
done = true;
|
||||||
|
_error = e;
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on('data', (data: Buffer) => {
|
||||||
|
buffers.push(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code: number, signal) => {
|
||||||
|
done = true;
|
||||||
|
process.removeListener('exit', cleanup);
|
||||||
|
if (buffers.length) {
|
||||||
|
_stdout = Buffer.concat(buffers).toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ stdout: _stdout, error });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const { stdout, error } = await promise;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// just swallow the error and move on with everything
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserShell() {
|
||||||
|
if (process.env.SHELL) {
|
||||||
|
return process.env.SHELL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/bin/bash';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the environment variables from the user's current shell and update the
|
||||||
|
* current environment.
|
||||||
|
*
|
||||||
|
* @param updateEnvironment a callback to fire if a valid environment is found
|
||||||
|
*/
|
||||||
|
async function getEnvironmentFromShell(
|
||||||
|
updateEnvironment: (env: IndexLookup) => void
|
||||||
|
): Promise<void> {
|
||||||
|
if (__WIN32__) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shellEnvText = await getRawShellEnv();
|
||||||
|
if (!shellEnvText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const env: IndexLookup = {};
|
||||||
|
|
||||||
|
for (const line of shellEnvText.split(os.EOL)) {
|
||||||
|
if (line.includes('=')) {
|
||||||
|
const components = line.split('=');
|
||||||
|
if (components.length === 2) {
|
||||||
|
env[components[0]] = components[1];
|
||||||
|
} else {
|
||||||
|
const k = components.shift();
|
||||||
|
const v = components.join('=');
|
||||||
|
if (k) {
|
||||||
|
env[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEnvironment(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply new environment variables to the current process, ignoring
|
||||||
|
* Node-specific environment variables which need to be preserved.
|
||||||
|
*
|
||||||
|
* @param env The new environment variables from the user's shell.
|
||||||
|
*/
|
||||||
|
function mergeEnvironmentVariables(env: IndexLookup) {
|
||||||
|
for (const key in env) {
|
||||||
|
if (BlacklistedNames.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env[key] = env[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current process's environment variables using environment
|
||||||
|
* variables from the user's shell, if they can be retrieved successfully.
|
||||||
|
*/
|
||||||
|
export function updateEnvironmentForProcess(): Promise<void> {
|
||||||
|
return getEnvironmentFromShell(mergeEnvironmentVariables);
|
||||||
|
}
|
137
@overflow/core/source-map-support.ts
Normal file
137
@overflow/core/source-map-support.ts
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import * as Path from 'path';
|
||||||
|
import * as Fs from 'fs';
|
||||||
|
|
||||||
|
const fileUriToPath: (uri: string) => string = require('file-uri-to-path');
|
||||||
|
const sourceMapSupport = require('source-map-support');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This array tells the source map logic which files that we can expect to
|
||||||
|
* be able to resolve a source map for and they should reflect the chunks
|
||||||
|
* entry names from our webpack config.
|
||||||
|
*
|
||||||
|
* Note that we explicitly don't enable source maps for the crash process
|
||||||
|
* since it's possible that the error which caused us to spawn the crash
|
||||||
|
* process was related to source maps.
|
||||||
|
*/
|
||||||
|
const knownFilesWithSourceMap = ['renderer.js', 'main.js'];
|
||||||
|
|
||||||
|
function retrieveSourceMap(source: string) {
|
||||||
|
// This is a happy path in case we know for certain that we won't be
|
||||||
|
// able to resolve a source map for the given location.
|
||||||
|
if (!knownFilesWithSourceMap.some(file => source.endsWith(file))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We get a file uri when we're inside a renderer, convert to a path
|
||||||
|
if (source.startsWith('file://')) {
|
||||||
|
source = fileUriToPath(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We store our source maps right next to the bundle
|
||||||
|
const path = `${source}.map`;
|
||||||
|
|
||||||
|
if (__DEV__ && path.startsWith('http://')) {
|
||||||
|
try {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', path, false);
|
||||||
|
xhr.send(null);
|
||||||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||||
|
return { url: Path.basename(path), map: xhr.responseText };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have an option here, see
|
||||||
|
// https://github.com/v8/v8/wiki/Stack-Trace-API#customizing-stack-traces
|
||||||
|
// This happens on-demand when someone accesses the stack
|
||||||
|
// property on an error object and has to be synchronous :/
|
||||||
|
// eslint-disable-next-line no-sync
|
||||||
|
if (!Fs.existsSync(path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-sync
|
||||||
|
const map = Fs.readFileSync(path, 'utf8');
|
||||||
|
return { url: Path.basename(path), map };
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A map from errors to their stack frames. */
|
||||||
|
const stackFrameMap = new WeakMap<Error, ReadonlyArray<any>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `prepareStackTrace` that comes from the `source-map-support` module.
|
||||||
|
* We'll use this when the user explicitly wants the stack source mapped.
|
||||||
|
*/
|
||||||
|
let prepareStackTraceWithSourceMap: (
|
||||||
|
error: Error,
|
||||||
|
frames: ReadonlyArray<any>
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture the error's stack frames and return a standard, un-source mapped
|
||||||
|
* stack trace.
|
||||||
|
*/
|
||||||
|
function prepareStackTrace(error: Error, frames: ReadonlyArray<any>) {
|
||||||
|
stackFrameMap.set(error, frames);
|
||||||
|
|
||||||
|
// Ideally we'd use the default `Error.prepareStackTrace` here but it's
|
||||||
|
// undefined so V8 must doing something fancy. Instead we'll do a decent
|
||||||
|
// impression.
|
||||||
|
return error + frames.map(frame => `\n at ${frame}`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enable source map support in the current process. */
|
||||||
|
export function enableSourceMaps() {
|
||||||
|
sourceMapSupport.install({
|
||||||
|
environment: 'node',
|
||||||
|
handleUncaughtExceptions: false,
|
||||||
|
retrieveSourceMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
const AnyError = Error as any;
|
||||||
|
// We want to keep `source-map-support`s `prepareStackTrace` around to use
|
||||||
|
// later, but our cheaper `prepareStackTrace` should be the default.
|
||||||
|
prepareStackTraceWithSourceMap = AnyError.prepareStackTrace;
|
||||||
|
AnyError.prepareStackTrace = prepareStackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a copy of the error with a source-mapped stack trace. If it couldn't
|
||||||
|
* perform the source mapping, it'll use the original error stack.
|
||||||
|
*/
|
||||||
|
export function withSourceMappedStack(error: Error): Error {
|
||||||
|
return {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: sourceMappedStackTrace(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the source mapped stack trace for the error. */
|
||||||
|
function sourceMappedStackTrace(error: Error): string | undefined {
|
||||||
|
let frames = stackFrameMap.get(error);
|
||||||
|
|
||||||
|
if (!frames) {
|
||||||
|
// At this point there's no guarantee that anyone has actually retrieved the
|
||||||
|
// stack on this error which means that our custom prepareStackTrace handler
|
||||||
|
// hasn't run and as a result of that we don't have the native frames stored
|
||||||
|
// in our weak map. In order to get around that we'll eagerly access the
|
||||||
|
// stack, forcing our handler to run which should ensure that the native
|
||||||
|
// frames are stored in our weak map.
|
||||||
|
(error.stack || '').toString();
|
||||||
|
frames = stackFrameMap.get(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frames) {
|
||||||
|
return error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepareStackTraceWithSourceMap(error, frames);
|
||||||
|
}
|
60
@overflow/core/window-state.ts
Normal file
60
@overflow/core/window-state.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// The name of the ipc channel over which state changes are communicated.
|
||||||
|
export const windowStateChannelName = 'window-state-changed';
|
||||||
|
|
||||||
|
export type WindowState =
|
||||||
|
| 'minimized'
|
||||||
|
| 'normal'
|
||||||
|
| 'maximized'
|
||||||
|
| 'full-screen'
|
||||||
|
| 'hidden';
|
||||||
|
|
||||||
|
export function getWindowState(window: Electron.BrowserWindow): WindowState {
|
||||||
|
if (window.isFullScreen()) {
|
||||||
|
return 'full-screen';
|
||||||
|
} else if (window.isMaximized()) {
|
||||||
|
return 'maximized';
|
||||||
|
} else if (window.isMinimized()) {
|
||||||
|
return 'minimized';
|
||||||
|
} else if (!window.isVisible()) {
|
||||||
|
return 'hidden';
|
||||||
|
} else {
|
||||||
|
return 'normal';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers event handlers for all window state transition events and
|
||||||
|
* forwards those to the renderer process for a given window.
|
||||||
|
*/
|
||||||
|
export function registerWindowStateChangedEvents(
|
||||||
|
window: Electron.BrowserWindow
|
||||||
|
) {
|
||||||
|
window.on('enter-full-screen', () =>
|
||||||
|
sendWindowStateEvent(window, 'full-screen')
|
||||||
|
);
|
||||||
|
|
||||||
|
// So this is a bit of a hack. If we call window.isFullScreen directly after
|
||||||
|
// receiving the leave-full-screen event it'll return true which isn't what
|
||||||
|
// we're after. So we'll say that we're transitioning to 'normal' even though
|
||||||
|
// we might be maximized. This works because electron will emit a 'maximized'
|
||||||
|
// event after 'leave-full-screen' if the state prior to full-screen was maximized.
|
||||||
|
window.on('leave-full-screen', () => sendWindowStateEvent(window, 'normal'));
|
||||||
|
|
||||||
|
window.on('maximize', () => sendWindowStateEvent(window, 'maximized'));
|
||||||
|
window.on('minimize', () => sendWindowStateEvent(window, 'minimized'));
|
||||||
|
window.on('unmaximize', () => sendWindowStateEvent(window, 'normal'));
|
||||||
|
window.on('restore', () => sendWindowStateEvent(window, 'normal'));
|
||||||
|
window.on('hide', () => sendWindowStateEvent(window, 'hidden'));
|
||||||
|
window.on('show', () => sendWindowStateEvent(window, 'normal'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short hand convenience function for sending a window state change event
|
||||||
|
* over the window-state-changed channel to the render process.
|
||||||
|
*/
|
||||||
|
function sendWindowStateEvent(
|
||||||
|
window: Electron.BrowserWindow,
|
||||||
|
state: WindowState
|
||||||
|
) {
|
||||||
|
window.webContents.send(windowStateChannelName, state);
|
||||||
|
}
|
|
@ -32,6 +32,13 @@ module.exports = function () {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
sourceMapFilename: '[file].map',
|
sourceMapFilename: '[file].map',
|
||||||
};
|
};
|
||||||
|
config.resolve = {
|
||||||
|
extensions: ['.ts', '.tsx', '.mjs', '.js'],
|
||||||
|
modules: [
|
||||||
|
root(),
|
||||||
|
'node_modules'
|
||||||
|
],
|
||||||
|
}
|
||||||
config.module = {
|
config.module = {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -47,6 +47,8 @@
|
||||||
"electron-connect": "^0.6.3",
|
"electron-connect": "^0.6.3",
|
||||||
"electron-debug": "^2.0.0",
|
"electron-debug": "^2.0.0",
|
||||||
"electron-window-state": "^4.1.1",
|
"electron-window-state": "^4.1.1",
|
||||||
|
"file-uri-to-path": "^1.0.0",
|
||||||
|
"file-url": "^2.0.2",
|
||||||
"fs-extra": "^7.0.0",
|
"fs-extra": "^7.0.0",
|
||||||
"jasmine-core": "~2.99.1",
|
"jasmine-core": "~2.99.1",
|
||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
|
@ -59,6 +61,7 @@
|
||||||
"primeng": "^6.0.0",
|
"primeng": "^6.0.0",
|
||||||
"primer-support": "^4.0.0",
|
"primer-support": "^4.0.0",
|
||||||
"protractor": "~5.3.0",
|
"protractor": "~5.3.0",
|
||||||
|
"source-map-support": "^0.5.8",
|
||||||
"ts-node": "~5.0.1",
|
"ts-node": "~5.0.1",
|
||||||
"tslint": "~5.9.1",
|
"tslint": "~5.9.1",
|
||||||
"typescript": "~2.7.2",
|
"typescript": "~2.7.2",
|
||||||
|
|
|
@ -1,215 +1,226 @@
|
||||||
// import { BrowserWindow, ipcMain, Menu, app, dialog } from 'electron';
|
import { BrowserWindow, ipcMain, Menu, app, dialog } from 'electron';
|
||||||
// import { encodePathAsUrl } from '../lib/path';
|
import * as path from 'path';
|
||||||
// import { registerWindowStateChangedEvents } from '../lib/window-state';
|
import * as URL from 'url';
|
||||||
// import { MenuEvent } from './menu';
|
import { EventEmitter } from 'events';
|
||||||
// import { URLActionType } from '../lib/parse-app-url';
|
|
||||||
// import { ILaunchStats } from '../lib/stats';
|
|
||||||
// import { menuFromElectronMenu } from '../models/app-menu';
|
|
||||||
// import { now } from './now';
|
|
||||||
// import * as path from 'path';
|
|
||||||
|
|
||||||
// let windowStateKeeper: any | null = null;
|
import { encodePathAsUrl } from '@overflow/core/path';
|
||||||
|
import { registerWindowStateChangedEvents } from '@overflow/core/window-state';
|
||||||
|
import { URLActionType } from '@overflow/core/parse-app-url';
|
||||||
|
import { now } from '@overflow/core/now';
|
||||||
|
|
||||||
// export class AppWindow {
|
import { MenuEvent } from './menu';
|
||||||
// private window: Electron.BrowserWindow;
|
|
||||||
// private emitter = new Emitter();
|
|
||||||
|
|
||||||
// private _loadTime: number | null = null;
|
let windowStateKeeper: any | null = null;
|
||||||
// private _rendererReadyTime: number | null = null;
|
|
||||||
|
|
||||||
// private minWidth = 960;
|
export class AppWindow {
|
||||||
// private minHeight = 660;
|
private window: Electron.BrowserWindow;
|
||||||
|
private emitter = new EventEmitter();
|
||||||
|
|
||||||
// public constructor() {
|
private _loadTime: number | null = null;
|
||||||
// if (!windowStateKeeper) {
|
private _rendererReadyTime: number | null = null;
|
||||||
// // `electron-window-state` requires Electron's `screen` module, which can
|
|
||||||
// // only be required after the app has emitted `ready`. So require it
|
|
||||||
// // lazily.
|
|
||||||
// windowStateKeeper = require('electron-window-state');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const savedWindowState = windowStateKeeper({
|
private minWidth = 960;
|
||||||
// defaultWidth: this.minWidth,
|
private minHeight = 660;
|
||||||
// defaultHeight: this.minHeight,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
public constructor() {
|
||||||
// x: savedWindowState.x,
|
if (!windowStateKeeper) {
|
||||||
// y: savedWindowState.y,
|
// `electron-window-state` requires Electron's `screen` module, which can
|
||||||
// width: savedWindowState.width,
|
// only be required after the app has emitted `ready`. So require it
|
||||||
// height: savedWindowState.height,
|
// lazily.
|
||||||
// minWidth: this.minWidth,
|
windowStateKeeper = require('electron-window-state');
|
||||||
// minHeight: this.minHeight,
|
}
|
||||||
// show: false,
|
|
||||||
// // This fixes subpixel aliasing on Windows
|
|
||||||
// // See https://github.com/atom/atom/commit/683bef5b9d133cb194b476938c77cc07fd05b972
|
|
||||||
// backgroundColor: '#fff',
|
|
||||||
// webPreferences: {
|
|
||||||
// // Disable auxclick event
|
|
||||||
// // See https://developers.google.com/web/updates/2016/10/auxclick
|
|
||||||
// disableBlinkFeatures: 'Auxclick',
|
|
||||||
// // Enable, among other things, the ResizeObserver
|
|
||||||
// experimentalFeatures: true,
|
|
||||||
// },
|
|
||||||
// acceptFirstMouse: true,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (__DARWIN__) {
|
const savedWindowState = windowStateKeeper({
|
||||||
// windowOptions.titleBarStyle = 'hidden';
|
defaultWidth: this.minWidth,
|
||||||
// } else if (__WIN32__) {
|
defaultHeight: this.minHeight,
|
||||||
// windowOptions.frame = false;
|
});
|
||||||
// } else if (__LINUX__) {
|
|
||||||
|
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
||||||
|
x: savedWindowState.x,
|
||||||
|
y: savedWindowState.y,
|
||||||
|
width: savedWindowState.width,
|
||||||
|
height: savedWindowState.height,
|
||||||
|
minWidth: this.minWidth,
|
||||||
|
minHeight: this.minHeight,
|
||||||
|
show: true,
|
||||||
|
// This fixes subpixel aliasing on Windows
|
||||||
|
// See https://github.com/atom/atom/commit/683bef5b9d133cb194b476938c77cc07fd05b972
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
webPreferences: {
|
||||||
|
// Disable auxclick event
|
||||||
|
// See https://developers.google.com/web/updates/2016/10/auxclick
|
||||||
|
disableBlinkFeatures: 'Auxclick',
|
||||||
|
// Enable, among other things, the ResizeObserver
|
||||||
|
experimentalFeatures: true,
|
||||||
|
},
|
||||||
|
acceptFirstMouse: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__DARWIN__) {
|
||||||
|
windowOptions.titleBarStyle = 'hidden';
|
||||||
|
} else if (__WIN32__) {
|
||||||
|
windowOptions.frame = false;
|
||||||
|
} else if (__LINUX__) {
|
||||||
// windowOptions.icon = path.join(__dirname, 'static', 'icon-logo.png');
|
// windowOptions.icon = path.join(__dirname, 'static', 'icon-logo.png');
|
||||||
// }
|
}
|
||||||
|
|
||||||
// this.window = new BrowserWindow(windowOptions);
|
console.log(windowOptions);
|
||||||
// savedWindowState.manage(this.window);
|
|
||||||
|
|
||||||
// let quitting = false;
|
this.window = new BrowserWindow(windowOptions);
|
||||||
// app.on('before-quit', () => {
|
savedWindowState.manage(this.window);
|
||||||
// quitting = true;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// ipcMain.on('will-quit', (event: Electron.IpcMessageEvent) => {
|
let quitting = false;
|
||||||
// quitting = true;
|
app.on('before-quit', () => {
|
||||||
// event.returnValue = true;
|
quitting = true;
|
||||||
// });
|
});
|
||||||
|
|
||||||
// // on macOS, when the user closes the window we really just hide it. This
|
ipcMain.on('will-quit', (event: Electron.IpcMessageEvent) => {
|
||||||
// // lets us activate quickly and keep all our interesting logic in the
|
quitting = true;
|
||||||
// // renderer.
|
event.returnValue = true;
|
||||||
// if (__DARWIN__) {
|
});
|
||||||
// this.window.on('close', e => {
|
|
||||||
// if (!quitting) {
|
|
||||||
// e.preventDefault();
|
|
||||||
// Menu.sendActionToFirstResponder('hide:');
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public load() {
|
// on macOS, when the user closes the window we really just hide it. This
|
||||||
// let startLoad = 0;
|
// lets us activate quickly and keep all our interesting logic in the
|
||||||
// // We only listen for the first of the loading events to avoid a bug in
|
// renderer.
|
||||||
// // Electron/Chromium where they can sometimes fire more than once. See
|
if (__DARWIN__) {
|
||||||
// // See
|
this.window.on('close', e => {
|
||||||
// // https://github.com/desktop/desktop/pull/513#issuecomment-253028277. This
|
if (!quitting) {
|
||||||
// // shouldn't really matter as in production builds loading _should_ only
|
e.preventDefault();
|
||||||
// // happen once.
|
Menu.sendActionToFirstResponder('hide:');
|
||||||
// this.window.webContents.once('did-start-loading', () => {
|
}
|
||||||
// this._rendererReadyTime = null;
|
});
|
||||||
// this._loadTime = null;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// startLoad = now();
|
public load() {
|
||||||
// });
|
let startLoad = 0;
|
||||||
|
// We only listen for the first of the loading events to avoid a bug in
|
||||||
|
// Electron/Chromium where they can sometimes fire more than once. See
|
||||||
|
// See
|
||||||
|
// https://github.com/desktop/desktop/pull/513#issuecomment-253028277. This
|
||||||
|
// shouldn't really matter as in production builds loading _should_ only
|
||||||
|
// happen once.
|
||||||
|
this.window.webContents.once('did-start-loading', () => {
|
||||||
|
this._rendererReadyTime = null;
|
||||||
|
this._loadTime = null;
|
||||||
|
|
||||||
// this.window.webContents.once('did-finish-load', () => {
|
startLoad = now();
|
||||||
// if (process.env.NODE_ENV === 'development') {
|
});
|
||||||
// this.window.webContents.openDevTools();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this._loadTime = now() - startLoad;
|
this.window.webContents.once('did-finish-load', () => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
this.window.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
// this.maybeEmitDidLoad();
|
this._loadTime = now() - startLoad;
|
||||||
// });
|
|
||||||
|
|
||||||
// this.window.webContents.on('did-finish-load', () => {
|
this.maybeEmitDidLoad();
|
||||||
// this.window.webContents.setVisualZoomLevelLimits(1, 1);
|
});
|
||||||
// });
|
|
||||||
|
|
||||||
// this.window.webContents.on('did-fail-load', () => {
|
this.window.webContents.on('did-finish-load', () => {
|
||||||
// this.window.webContents.openDevTools();
|
this.window.webContents.setVisualZoomLevelLimits(1, 1);
|
||||||
// this.window.show();
|
});
|
||||||
// });
|
|
||||||
|
|
||||||
// // TODO: This should be scoped by the window.
|
this.window.webContents.on('did-fail-load', () => {
|
||||||
// ipcMain.once(
|
this.window.webContents.openDevTools();
|
||||||
// 'renderer-ready',
|
this.window.show();
|
||||||
// (event: Electron.IpcMessageEvent, readyTime: number) => {
|
});
|
||||||
// this._rendererReadyTime = readyTime;
|
|
||||||
|
|
||||||
// this.maybeEmitDidLoad();
|
// TODO: This should be scoped by the window.
|
||||||
// }
|
ipcMain.once(
|
||||||
// );
|
'renderer-ready',
|
||||||
|
(event: Electron.IpcMessageEvent, readyTime: number) => {
|
||||||
|
this._rendererReadyTime = readyTime;
|
||||||
|
|
||||||
// this.window.on('focus', () => this.window.webContents.send('focus'));
|
this.maybeEmitDidLoad();
|
||||||
// this.window.on('blur', () => this.window.webContents.send('blur'));
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// registerWindowStateChangedEvents(this.window);
|
this.window.on('focus', () => this.window.webContents.send('focus'));
|
||||||
|
this.window.on('blur', () => this.window.webContents.send('blur'));
|
||||||
|
|
||||||
|
registerWindowStateChangedEvents(this.window);
|
||||||
// this.window.loadURL(encodePathAsUrl(__dirname, 'index.html'));
|
// this.window.loadURL(encodePathAsUrl(__dirname, 'index.html'));
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
const indexUrl = URL.format({
|
||||||
// * Emit the `onDidLoad` event if the page has loaded and the renderer has
|
pathname: path.join('//localhost:4200'),
|
||||||
// * signalled that it's ready.
|
protocol: 'http:',
|
||||||
// */
|
slashes: true
|
||||||
// private maybeEmitDidLoad() {
|
});
|
||||||
// if (!this.rendererLoaded) {
|
this.window.loadURL(indexUrl);
|
||||||
// return;
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// this.emitter.emit('did-load', null);
|
/**
|
||||||
// }
|
* Emit the `onDidLoad` event if the page has loaded and the renderer has
|
||||||
|
* signalled that it's ready.
|
||||||
|
*/
|
||||||
|
private maybeEmitDidLoad() {
|
||||||
|
if (!this.rendererLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// /** Is the page loaded and has the renderer signalled it's ready? */
|
this.emitter.emit('did-load', null);
|
||||||
// private get rendererLoaded(): boolean {
|
}
|
||||||
// return !!this.loadTime && !!this.rendererReadyTime;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public onClose(fn: () => void) {
|
/** Is the page loaded and has the renderer signalled it's ready? */
|
||||||
// this.window.on('closed', fn);
|
private get rendererLoaded(): boolean {
|
||||||
// }
|
return !!this.loadTime && !!this.rendererReadyTime;
|
||||||
|
}
|
||||||
|
|
||||||
// /**
|
public onClose(fn: () => void) {
|
||||||
// * Register a function to call when the window is done loading. At that point
|
this.window.on('closed', fn);
|
||||||
// * the page has loaded and the renderer has signalled that it is ready.
|
}
|
||||||
// */
|
|
||||||
// public onDidLoad(fn: () => void): Disposable {
|
|
||||||
// return this.emitter.on('did-load', fn);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public isMinimized() {
|
/**
|
||||||
// return this.window.isMinimized();
|
* Register a function to call when the window is done loading. At that point
|
||||||
// }
|
* the page has loaded and the renderer has signalled that it is ready.
|
||||||
|
*/
|
||||||
|
public onDidLoad(fn: () => void): EventEmitter {
|
||||||
|
return this.emitter.on('did-load', fn);
|
||||||
|
}
|
||||||
|
|
||||||
// /** Is the window currently visible? */
|
public isMinimized() {
|
||||||
// public isVisible() {
|
return this.window.isMinimized();
|
||||||
// return this.window.isVisible();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// public restore() {
|
/** Is the window currently visible? */
|
||||||
// this.window.restore();
|
public isVisible() {
|
||||||
// }
|
return this.window.isVisible();
|
||||||
|
}
|
||||||
|
|
||||||
// public focus() {
|
public restore() {
|
||||||
// this.window.focus();
|
this.window.restore();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /** Show the window. */
|
public focus() {
|
||||||
// public show() {
|
this.window.focus();
|
||||||
// this.window.show();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// /** Send the menu event to the renderer. */
|
/** Show the window. */
|
||||||
// public sendMenuEvent(name: MenuEvent) {
|
public show() {
|
||||||
// this.show();
|
this.window.show();
|
||||||
|
}
|
||||||
|
|
||||||
// this.window.webContents.send('menu-event', { name });
|
/** Send the menu event to the renderer. */
|
||||||
// }
|
public sendMenuEvent(name: MenuEvent) {
|
||||||
|
this.show();
|
||||||
|
|
||||||
// /** Send the URL action to the renderer. */
|
this.window.webContents.send('menu-event', { name });
|
||||||
// public sendURLAction(action: URLActionType) {
|
}
|
||||||
// this.show();
|
|
||||||
|
|
||||||
// this.window.webContents.send('url-action', { action });
|
/** Send the URL action to the renderer. */
|
||||||
// }
|
public sendURLAction(action: URLActionType) {
|
||||||
|
this.show();
|
||||||
|
|
||||||
// /** Send the app launch timing stats to the renderer. */
|
this.window.webContents.send('url-action', { action });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send the app launch timing stats to the renderer. */
|
||||||
// public sendLaunchTimingStats(stats: ILaunchStats) {
|
// public sendLaunchTimingStats(stats: ILaunchStats) {
|
||||||
// this.window.webContents.send('launch-timing-stats', { stats });
|
// this.window.webContents.send('launch-timing-stats', { stats });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// /** Send the app menu to the renderer. */
|
/** Send the app menu to the renderer. */
|
||||||
// public sendAppMenu() {
|
// public sendAppMenu() {
|
||||||
// const appMenu = Menu.getApplicationMenu();
|
// const appMenu = Menu.getApplicationMenu();
|
||||||
// if (appMenu) {
|
// if (appMenu) {
|
||||||
|
@ -218,65 +229,65 @@
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// /** Send a certificate error to the renderer. */
|
/** Send a certificate error to the renderer. */
|
||||||
// public sendCertificateError(
|
public sendCertificateError(
|
||||||
// certificate: Electron.Certificate,
|
certificate: Electron.Certificate,
|
||||||
// error: string,
|
error: string,
|
||||||
// url: string
|
url: string
|
||||||
// ) {
|
) {
|
||||||
// this.window.webContents.send('certificate-error', {
|
this.window.webContents.send('certificate-error', {
|
||||||
// certificate,
|
certificate,
|
||||||
// error,
|
error,
|
||||||
// url,
|
url,
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public showCertificateTrustDialog(
|
public showCertificateTrustDialog(
|
||||||
// certificate: Electron.Certificate,
|
certificate: Electron.Certificate,
|
||||||
// message: string
|
message: string
|
||||||
// ) {
|
) {
|
||||||
// // The Electron type definitions don't include `showCertificateTrustDialog`
|
// The Electron type definitions don't include `showCertificateTrustDialog`
|
||||||
// // yet.
|
// yet.
|
||||||
// const d = dialog as any;
|
const d = dialog as any;
|
||||||
// d.showCertificateTrustDialog(
|
d.showCertificateTrustDialog(
|
||||||
// this.window,
|
this.window,
|
||||||
// { certificate, message },
|
{ certificate, message },
|
||||||
// () => { }
|
() => { }
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /** Report the exception to the renderer. */
|
/** Report the exception to the renderer. */
|
||||||
// public sendException(error: Error) {
|
public sendException(error: Error) {
|
||||||
// // `Error` can't be JSONified so it doesn't transport nicely over IPC. So
|
// `Error` can't be JSONified so it doesn't transport nicely over IPC. So
|
||||||
// // we'll just manually copy the properties we care about.
|
// we'll just manually copy the properties we care about.
|
||||||
// const friendlyError = {
|
const friendlyError = {
|
||||||
// stack: error.stack,
|
stack: error.stack,
|
||||||
// message: error.message,
|
message: error.message,
|
||||||
// name: error.name,
|
name: error.name,
|
||||||
// };
|
};
|
||||||
// this.window.webContents.send('main-process-exception', friendlyError);
|
this.window.webContents.send('main-process-exception', friendlyError);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Get the time (in milliseconds) spent loading the page.
|
* Get the time (in milliseconds) spent loading the page.
|
||||||
// *
|
*
|
||||||
// * This will be `null` until `onDidLoad` is called.
|
* This will be `null` until `onDidLoad` is called.
|
||||||
// */
|
*/
|
||||||
// public get loadTime(): number | null {
|
public get loadTime(): number | null {
|
||||||
// return this._loadTime;
|
return this._loadTime;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Get the time (in milliseconds) elapsed from the renderer being loaded to it
|
* Get the time (in milliseconds) elapsed from the renderer being loaded to it
|
||||||
// * signaling it was ready.
|
* signaling it was ready.
|
||||||
// *
|
*
|
||||||
// * This will be `null` until `onDidLoad` is called.
|
* This will be `null` until `onDidLoad` is called.
|
||||||
// */
|
*/
|
||||||
// public get rendererReadyTime(): number | null {
|
public get rendererReadyTime(): number | null {
|
||||||
// return this._rendererReadyTime;
|
return this._rendererReadyTime;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public destroy() {
|
public destroy() {
|
||||||
// this.window.destroy();
|
this.window.destroy();
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
|
@ -1,74 +1,461 @@
|
||||||
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
|
import { app, Menu, ipcMain, BrowserWindow, shell } from 'electron';
|
||||||
import * as path from 'path';
|
import * as Fs from 'fs';
|
||||||
import * as url from 'url';
|
|
||||||
|
|
||||||
// const indexUrl = url.format({
|
import { shellNeedsPatching, updateEnvironmentForProcess } from '@overflow/core/shell';
|
||||||
// pathname: path.join(__dirname, 'index.html'),
|
import { parseAppURL } from '@overflow/core/parse-app-url';
|
||||||
// protocol: 'file:',
|
import {
|
||||||
// slashes: true
|
enableSourceMaps,
|
||||||
// });
|
withSourceMappedStack,
|
||||||
|
} from '@overflow/core/source-map-support';
|
||||||
|
import { now } from '@overflow/core/now';
|
||||||
|
import { IMenuItem } from '@overflow/core/menu-item';
|
||||||
|
|
||||||
const indexUrl = url.format({
|
import { AppWindow } from './app-window';
|
||||||
pathname: path.join('//localhost:4200'),
|
|
||||||
protocol: 'http:',
|
import { handleSquirrelEvent } from './squirrel-updater';
|
||||||
slashes: true
|
|
||||||
|
import { openDirectorySafe } from './shell';
|
||||||
|
|
||||||
|
enableSourceMaps();
|
||||||
|
|
||||||
|
let mainWindow: AppWindow | null = null;
|
||||||
|
|
||||||
|
const launchTime = now();
|
||||||
|
|
||||||
|
let preventQuit = false;
|
||||||
|
let readyTime: number | null = null;
|
||||||
|
|
||||||
|
type OnDidLoadFn = (window: AppWindow) => void;
|
||||||
|
/** See the `onDidLoad` function. */
|
||||||
|
const onDidLoadFns: Array<OnDidLoadFn> | null = [];
|
||||||
|
|
||||||
|
function handleUncaughtException(error: Error) {
|
||||||
|
preventQuit = true;
|
||||||
|
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.destroy();
|
||||||
|
mainWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLaunchError = !mainWindow;
|
||||||
|
// showUncaughtException(isLaunchError, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error: Error) => {
|
||||||
|
error = withSourceMappedStack(error);
|
||||||
|
|
||||||
|
// reportError(error);
|
||||||
|
handleUncaughtException(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let handlingSquirrelEvent = false;
|
||||||
|
if (__WIN32__ && process.argv.length > 1) {
|
||||||
|
const arg = process.argv[1];
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
const promise = handleSquirrelEvent(arg);
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
if (promise) {
|
||||||
let win;
|
handlingSquirrelEvent = true;
|
||||||
|
promise
|
||||||
|
.catch(e => {
|
||||||
|
log.error(`Failed handling Squirrel event: ${arg}`, e);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handlePossibleProtocolLauncherArgs(process.argv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function handleAppURL(url: string) {
|
||||||
// Create the browser window.
|
log.info('Processing protocol url');
|
||||||
win = new BrowserWindow({ width: 800, height: 600 });
|
const action = parseAppURL(url);
|
||||||
|
onDidLoad(window => {
|
||||||
// and load the index.html of the app.
|
// This manual focus call _shouldn't_ be necessary, but is for Chrome on
|
||||||
win.loadURL(indexUrl);
|
// macOS. See https://github.com/desktop/desktop/issues/973.
|
||||||
|
window.focus();
|
||||||
// Open the DevTools.
|
window.sendURLAction(action);
|
||||||
win.webContents.openDevTools();
|
|
||||||
|
|
||||||
// Emitted when the window is closed.
|
|
||||||
win.on('closed', () => {
|
|
||||||
// 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.
|
|
||||||
win = null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
let isDuplicateInstance = false;
|
||||||
// initialization and is ready to create browser windows.
|
// If we're handling a Squirrel event we don't want to enforce single instance.
|
||||||
// Some APIs can only be used after this event occurs.
|
// We want to let the updated instance launch and do its work. It will then quit
|
||||||
app.on('ready', createWindow);
|
// once it's done.
|
||||||
|
if (!handlingSquirrelEvent) {
|
||||||
|
isDuplicateInstance = app.makeSingleInstance((args, workingDirectory) => {
|
||||||
|
// Someone tried to run a second instance, we should focus our window.
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMinimized()) {
|
||||||
|
mainWindow.restore();
|
||||||
|
}
|
||||||
|
|
||||||
// Quit when all windows are closed.
|
if (!mainWindow.isVisible()) {
|
||||||
app.on('window-all-closed', () => {
|
mainWindow.show();
|
||||||
// On macOS it is common for applications and their menu bar
|
}
|
||||||
// to stay active until the user quits explicitly with Cmd + Q
|
|
||||||
if (process.platform !== 'darwin') {
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePossibleProtocolLauncherArgs(args);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDuplicateInstance) {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shellNeedsPatching(process)) {
|
||||||
|
updateEnvironmentForProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('will-finish-launching', () => {
|
||||||
|
// macOS only
|
||||||
|
app.on('open-url', (event, url) => {
|
||||||
|
event.preventDefault();
|
||||||
|
handleAppURL(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to detect and handle any protocol handler arguments passed
|
||||||
|
* either via the command line directly to the current process or through
|
||||||
|
* IPC from a duplicate instance (see makeSingleInstance)
|
||||||
|
*
|
||||||
|
* @param args Essentially process.argv, i.e. the first element is the exec
|
||||||
|
* path
|
||||||
|
*/
|
||||||
|
function handlePossibleProtocolLauncherArgs(args: ReadonlyArray<string>) {
|
||||||
|
log.info(`Received possible protocol arguments: ${args.length}`);
|
||||||
|
|
||||||
|
if (__WIN32__) {
|
||||||
|
// We register our protocol handler callback on Windows as
|
||||||
|
// [executable path] --protocol-launcher -- "%1" meaning that any
|
||||||
|
// url data comes after we've stopped processing arguments. We check
|
||||||
|
// for that exact scenario here before doing any processing. If there's
|
||||||
|
// more than 4 args because of a malformed url then we bail out.
|
||||||
|
if (
|
||||||
|
args.length === 4 &&
|
||||||
|
args[1] === '--protocol-launcher' &&
|
||||||
|
args[2] === '--'
|
||||||
|
) {
|
||||||
|
handleAppURL(args[3]);
|
||||||
|
}
|
||||||
|
} else if (args.length > 1) {
|
||||||
|
handleAppURL(args[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around app.setAsDefaultProtocolClient that adds our
|
||||||
|
* custom prefix command line switches on Windows that prevents
|
||||||
|
* command line argument parsing after the `--`.
|
||||||
|
*/
|
||||||
|
function setAsDefaultProtocolClient(protocol: string) {
|
||||||
|
if (__WIN32__) {
|
||||||
|
app.setAsDefaultProtocolClient(protocol, process.execPath, [
|
||||||
|
'--protocol-launcher',
|
||||||
|
'--',
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
app.setAsDefaultProtocolClient(protocol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (process.env.GITHUB_DESKTOP_DISABLE_HARDWARE_ACCELERATION) {
|
||||||
|
// log.info(
|
||||||
|
// `GITHUB_DESKTOP_DISABLE_HARDWARE_ACCELERATION environment variable set, disabling hardware acceleration`
|
||||||
|
// );
|
||||||
|
// app.disableHardwareAcceleration();
|
||||||
|
// }
|
||||||
|
|
||||||
|
app.on('ready', () => {
|
||||||
|
if (isDuplicateInstance || handlingSquirrelEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readyTime = now() - launchTime;
|
||||||
|
|
||||||
|
// setAsDefaultProtocolClient('x-github-client');
|
||||||
|
|
||||||
|
// if (__DEV__) {
|
||||||
|
// setAsDefaultProtocolClient('x-github-desktop-dev-auth');
|
||||||
|
// } else {
|
||||||
|
// setAsDefaultProtocolClient('x-github-desktop-auth');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Also support Desktop Classic's protocols.
|
||||||
|
// if (__DARWIN__) {
|
||||||
|
// setAsDefaultProtocolClient('github-mac');
|
||||||
|
// } else if (__WIN32__) {
|
||||||
|
// setAsDefaultProtocolClient('github-windows');
|
||||||
|
// }
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
// let menu = buildDefaultMenu();
|
||||||
|
// Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'update-preferred-app-menu-item-labels',
|
||||||
|
// (
|
||||||
|
// event: Electron.IpcMessageEvent,
|
||||||
|
// labels: { editor?: string; pullRequestLabel?: string; shell: string }
|
||||||
|
// ) => {
|
||||||
|
// menu = buildDefaultMenu(
|
||||||
|
// labels.editor,
|
||||||
|
// labels.shell,
|
||||||
|
// labels.pullRequestLabel
|
||||||
|
// );
|
||||||
|
// Menu.setApplicationMenu(menu);
|
||||||
|
// if (mainWindow) {
|
||||||
|
// mainWindow.sendAppMenu();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ipcMain.on('menu-event', (event: Electron.IpcMessageEvent, args: any[]) => {
|
||||||
|
// const { name }: { name: MenuEvent } = event as any;
|
||||||
|
// if (mainWindow) {
|
||||||
|
// mainWindow.sendMenuEvent(name);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event sent by the renderer asking that the menu item with the given id
|
||||||
|
* is executed (ie clicked).
|
||||||
|
*/
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'execute-menu-item',
|
||||||
|
// (event: Electron.IpcMessageEvent, { id }: { id: string }) => {
|
||||||
|
// const menuItem = findMenuItemByID(menu, id);
|
||||||
|
// if (menuItem) {
|
||||||
|
// const window = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
// const fakeEvent = { preventDefault: () => { }, sender: event.sender };
|
||||||
|
// menuItem.click(fakeEvent, window, event.sender);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'update-menu-state',
|
||||||
|
// (
|
||||||
|
// event: Electron.IpcMessageEvent,
|
||||||
|
// items: Array<{ id: string; state: IMenuItemState }>
|
||||||
|
// ) => {
|
||||||
|
// let sendMenuChangedEvent = false;
|
||||||
|
|
||||||
|
// for (const item of items) {
|
||||||
|
// const { id, state } = item;
|
||||||
|
// const menuItem = findMenuItemByID(menu, id);
|
||||||
|
|
||||||
|
// if (menuItem) {
|
||||||
|
// // Only send the updated app menu when the state actually changes
|
||||||
|
// // or we might end up introducing a never ending loop between
|
||||||
|
// // the renderer and the main process
|
||||||
|
// if (
|
||||||
|
// state.enabled !== undefined &&
|
||||||
|
// menuItem.enabled !== state.enabled
|
||||||
|
// ) {
|
||||||
|
// menuItem.enabled = state.enabled;
|
||||||
|
// sendMenuChangedEvent = true;
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// fatalError(`Unknown menu id: ${id}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (sendMenuChangedEvent && mainWindow) {
|
||||||
|
// mainWindow.sendAppMenu();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'show-contextual-menu',
|
||||||
|
// (event: Electron.IpcMessageEvent, items: ReadonlyArray<IMenuItem>) => {
|
||||||
|
// const menu = buildContextMenu(items, ix =>
|
||||||
|
// event.sender.send('contextual-menu-action', ix)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const window = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
// menu.popup({ window });
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event sent by the renderer asking for a copy of the current
|
||||||
|
* application menu.
|
||||||
|
*/
|
||||||
|
// ipcMain.on('get-app-menu', () => {
|
||||||
|
// if (mainWindow) {
|
||||||
|
// mainWindow.sendAppMenu();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'show-certificate-trust-dialog',
|
||||||
|
// (
|
||||||
|
// event: Electron.IpcMessageEvent,
|
||||||
|
// {
|
||||||
|
// certificate,
|
||||||
|
// message,
|
||||||
|
// }: { certificate: Electron.Certificate; message: string }
|
||||||
|
// ) => {
|
||||||
|
// // This API is only implemented for macOS and Windows right now.
|
||||||
|
// if (__DARWIN__ || __WIN32__) {
|
||||||
|
// onDidLoad(window => {
|
||||||
|
// window.showCertificateTrustDialog(certificate, message);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'log',
|
||||||
|
// (event: Electron.IpcMessageEvent, level: LogLevel, message: string) => {
|
||||||
|
// writeLog(level, message);
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'uncaught-exception',
|
||||||
|
// (event: Electron.IpcMessageEvent, error: Error) => {
|
||||||
|
// handleUncaughtException(error);
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'send-error-report',
|
||||||
|
// (
|
||||||
|
// event: Electron.IpcMessageEvent,
|
||||||
|
// { error, extra }: { error: Error; extra: { [key: string]: string } }
|
||||||
|
// ) => {
|
||||||
|
// reportError(error, extra);
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'open-external',
|
||||||
|
// (event: Electron.IpcMessageEvent, { path }: { path: string }) => {
|
||||||
|
// const pathLowerCase = path.toLowerCase();
|
||||||
|
// if (
|
||||||
|
// pathLowerCase.startsWith('http://') ||
|
||||||
|
// pathLowerCase.startsWith('https://')
|
||||||
|
// ) {
|
||||||
|
// log.info(`opening in browser: ${path}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const result = shell.openExternal(path);
|
||||||
|
// event.sender.send('open-external-result', { result });
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ipcMain.on(
|
||||||
|
// 'show-item-in-folder',
|
||||||
|
// (event: Electron.IpcMessageEvent, { path }: { path: string }) => {
|
||||||
|
// Fs.stat(path, (err, stats) => {
|
||||||
|
// if (err) {
|
||||||
|
// log.error(`Unable to find file at '${path}'`, err);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (stats.isDirectory()) {
|
||||||
|
// openDirectorySafe(path);
|
||||||
|
// } else {
|
||||||
|
// shell.showItemInFolder(path);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
onDidLoad(window => {
|
||||||
|
window.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('web-contents-created', (event, contents) => {
|
||||||
|
contents.on('new-window', (_event, url) => {
|
||||||
|
// Prevent links or window.open from opening new windows
|
||||||
|
_event.preventDefault();
|
||||||
|
log.warn(`Prevented new window to: ${url}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// app.on(
|
||||||
|
// 'certificate-error',
|
||||||
|
// (event, webContents, url, error, certificate, callback) => {
|
||||||
|
// callback(false);
|
||||||
|
|
||||||
|
// onDidLoad(window => {
|
||||||
|
// window.sendCertificateError(certificate, error, url);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
const window = new AppWindow();
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
const {
|
||||||
|
default: installExtension,
|
||||||
|
} = require('electron-devtools-installer');
|
||||||
|
|
||||||
|
require('electron-debug')({ showDevTools: true });
|
||||||
|
|
||||||
|
const ChromeLens = {
|
||||||
|
id: 'idikgljglpfilbhaboonnpnnincjhjkd',
|
||||||
|
electron: '>=1.2.1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const extensions = [ChromeLens];
|
||||||
|
|
||||||
|
for (const extension of extensions) {
|
||||||
|
try {
|
||||||
|
installExtension(extension);
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onClose(() => {
|
||||||
|
mainWindow = null;
|
||||||
|
if (!__DARWIN__ && !preventQuit) {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
window.onDidLoad(() => {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
window.show();
|
||||||
// dock icon is clicked and there are no other windows open.
|
// window.sendLaunchTimingStats({
|
||||||
if (win === null) {
|
// mainReadyTime: readyTime!,
|
||||||
createWindow();
|
// loadTime: window.loadTime!,
|
||||||
|
// rendererReadyTime: window.rendererReadyTime!,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const fns = onDidLoadFns!;
|
||||||
|
// onDidLoadFns = null;
|
||||||
|
// for (const fn of fns) {
|
||||||
|
// fn(window);
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
window.load();
|
||||||
|
|
||||||
|
mainWindow = window;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// In this file you can include the rest of your app's specific main process
|
/**
|
||||||
// code. You can also put them in separate files and require them here.
|
* Register a function to be called once the window has been loaded. If the
|
||||||
|
* window has already been loaded, the function will be called immediately.
|
||||||
ipcMain.on('show-dialog', (event, arg) => {
|
*/
|
||||||
dialog.showMessageBox(win, {
|
function onDidLoad(fn: OnDidLoadFn) {
|
||||||
type: 'info',
|
if (onDidLoadFns) {
|
||||||
buttons: ['OK'],
|
onDidLoadFns.push(fn);
|
||||||
title: 'Native Dialog',
|
} else {
|
||||||
message: 'I\'m a native dialog!',
|
if (mainWindow) {
|
||||||
detail: 'It\'s my pleasure to make your life better.'
|
fn(mainWindow);
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
2
src/electron/menu/index.ts
Normal file
2
src/electron/menu/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './menu-event';
|
||||||
|
export * from './menu-ids';
|
4
src/electron/menu/menu-event.ts
Normal file
4
src/electron/menu/menu-event.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export type MenuEvent =
|
||||||
|
| 'show-about'
|
||||||
|
| 'open-external-editor'
|
||||||
|
| 'select-all';
|
5
src/electron/menu/menu-ids.ts
Normal file
5
src/electron/menu/menu-ids.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type MenuIDs =
|
||||||
|
| 'preferences'
|
||||||
|
| 'open-in-shell'
|
||||||
|
| 'open-external-editor'
|
||||||
|
| 'about';
|
25
src/electron/shell.ts
Normal file
25
src/electron/shell.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import * as Url from 'url';
|
||||||
|
import { shell } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the inbuilt shell.openItem path to address a focus issue that affects macOS.
|
||||||
|
*
|
||||||
|
* When opening a folder in Finder, the window will appear behind the application
|
||||||
|
* window, which may confuse users. As a workaround, we will fallback to using
|
||||||
|
* shell.openExternal for macOS until it can be fixed upstream.
|
||||||
|
*
|
||||||
|
* @param path directory to open
|
||||||
|
*/
|
||||||
|
export function openDirectorySafe(path: string) {
|
||||||
|
if (__DARWIN__) {
|
||||||
|
const directoryURL = Url.format({
|
||||||
|
pathname: path,
|
||||||
|
protocol: 'file:',
|
||||||
|
slashes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
shell.openExternal(directoryURL);
|
||||||
|
} else {
|
||||||
|
shell.openItem(path);
|
||||||
|
}
|
||||||
|
}
|
156
src/electron/squirrel-updater.ts
Normal file
156
src/electron/squirrel-updater.ts
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import * as Path from 'path';
|
||||||
|
import * as Os from 'os';
|
||||||
|
|
||||||
|
import { pathExists, ensureDir, writeFile } from 'fs-extra';
|
||||||
|
import { spawn, getPathSegments, setPathSegments } from '@overflow/core/process/win32';
|
||||||
|
|
||||||
|
const appFolder = Path.resolve(process.execPath, '..');
|
||||||
|
const rootAppDir = Path.resolve(appFolder, '..');
|
||||||
|
const updateDotExe = Path.resolve(Path.join(rootAppDir, 'Update.exe'));
|
||||||
|
const exeName = Path.basename(process.execPath);
|
||||||
|
|
||||||
|
// A lot of this code was cargo-culted from our Atom comrades:
|
||||||
|
// https://github.com/atom/atom/blob/7c9f39e3f1d05ee423e0093e6b83f042ce11c90a/src/main-process/squirrel-update.coffee.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Squirrel.Windows app lifecycle events.
|
||||||
|
*
|
||||||
|
* Returns a promise which will resolve when the work is done.
|
||||||
|
*/
|
||||||
|
export function handleSquirrelEvent(eventName: string): Promise<void> | null {
|
||||||
|
switch (eventName) {
|
||||||
|
case '--squirrel-install':
|
||||||
|
return handleInstalled();
|
||||||
|
|
||||||
|
case '--squirrel-updated':
|
||||||
|
return handleUpdated();
|
||||||
|
|
||||||
|
case '--squirrel-uninstall':
|
||||||
|
return handleUninstall();
|
||||||
|
|
||||||
|
case '--squirrel-obsolete':
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleInstalled(): Promise<void> {
|
||||||
|
await createShortcut(['StartMenu', 'Desktop']);
|
||||||
|
await installCLI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdated(): Promise<void> {
|
||||||
|
await updateShortcut();
|
||||||
|
await installCLI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installCLI(): Promise<void> {
|
||||||
|
const binPath = getBinPath();
|
||||||
|
await ensureDir(binPath);
|
||||||
|
await writeBatchScriptCLITrampoline(binPath);
|
||||||
|
await writeShellScriptCLITrampoline(binPath);
|
||||||
|
const paths = await getPathSegments();
|
||||||
|
if (paths.indexOf(binPath) < 0) {
|
||||||
|
await setPathSegments([...paths, binPath]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path for the `bin` directory which exists in our `AppData` but
|
||||||
|
* outside path which includes the installed app version.
|
||||||
|
*/
|
||||||
|
function getBinPath(): string {
|
||||||
|
return Path.resolve(process.execPath, '../../bin');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveVersionedPath(binPath: string, relativePath: string): string {
|
||||||
|
const _appFolder = Path.resolve(process.execPath, '..');
|
||||||
|
return Path.relative(binPath, Path.join(_appFolder, relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here's the problem: our app's path contains its version number. So each time
|
||||||
|
* we update, the path to our app changes. So it's Real Hard to add our path
|
||||||
|
* directly to `Path`. We'd have to detect and remove stale entries, etc.
|
||||||
|
*
|
||||||
|
* So instead, we write a trampoline out to a fixed path, still inside our
|
||||||
|
* `AppData` directory but outside the version-specific path. That trampoline
|
||||||
|
* just launches the current version's CLI tool. Then, whenever we update, we
|
||||||
|
* rewrite the trampoline to point to the new, version-specific path. Bingo
|
||||||
|
* bango Bob's your uncle.
|
||||||
|
*/
|
||||||
|
function writeBatchScriptCLITrampoline(binPath: string): Promise<void> {
|
||||||
|
const versionedPath = resolveVersionedPath(
|
||||||
|
binPath,
|
||||||
|
'resources/app/static/github.bat'
|
||||||
|
);
|
||||||
|
|
||||||
|
const trampoline = `@echo off\n"%~dp0\\${versionedPath}" %*`;
|
||||||
|
const trampolinePath = Path.join(binPath, 'github.bat');
|
||||||
|
|
||||||
|
return writeFile(trampolinePath, trampoline);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeShellScriptCLITrampoline(binPath: string): Promise<void> {
|
||||||
|
const versionedPath = resolveVersionedPath(
|
||||||
|
binPath,
|
||||||
|
'resources/app/static/github.sh'
|
||||||
|
);
|
||||||
|
|
||||||
|
const trampoline = `#!/usr/bin/env bash
|
||||||
|
DIR="$( cd "$( dirname "\$\{BASH_SOURCE[0]\}" )" && pwd )"
|
||||||
|
sh "$DIR/${versionedPath}" "$@"`;
|
||||||
|
const trampolinePath = Path.join(binPath, 'github');
|
||||||
|
|
||||||
|
return writeFile(trampolinePath, trampoline, { encoding: 'utf8', mode: 755 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spawn the Squirrel.Windows `Update.exe` with a command. */
|
||||||
|
async function spawnSquirrelUpdate(
|
||||||
|
commands: ReadonlyArray<string>
|
||||||
|
): Promise<void> {
|
||||||
|
await spawn(updateDotExe, commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortcutLocations = ReadonlyArray<'StartMenu' | 'Desktop'>;
|
||||||
|
|
||||||
|
function createShortcut(locations: ShortcutLocations): Promise<void> {
|
||||||
|
return spawnSquirrelUpdate([
|
||||||
|
'--createShortcut',
|
||||||
|
exeName,
|
||||||
|
'-l',
|
||||||
|
locations.join(','),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUninstall(): Promise<void> {
|
||||||
|
await removeShortcut();
|
||||||
|
|
||||||
|
const paths = await getPathSegments();
|
||||||
|
const binPath = getBinPath();
|
||||||
|
const pathsWithoutBinPath = paths.filter(p => p !== binPath);
|
||||||
|
return setPathSegments(pathsWithoutBinPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeShortcut(): Promise<void> {
|
||||||
|
return spawnSquirrelUpdate(['--removeShortcut', exeName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateShortcut(): Promise<void> {
|
||||||
|
const homeDirectory = Os.homedir();
|
||||||
|
if (homeDirectory) {
|
||||||
|
const desktopShortcutPath = Path.join(
|
||||||
|
homeDirectory,
|
||||||
|
'Desktop',
|
||||||
|
'GitHub Desktop.lnk'
|
||||||
|
);
|
||||||
|
const exists = await pathExists(desktopShortcutPath);
|
||||||
|
const locations: ShortcutLocations = exists
|
||||||
|
? ['StartMenu', 'Desktop']
|
||||||
|
: ['StartMenu'];
|
||||||
|
return createShortcut(locations);
|
||||||
|
} else {
|
||||||
|
return createShortcut(['StartMenu', 'Desktop']);
|
||||||
|
}
|
||||||
|
}
|
256
src/globals.d.ts
vendored
Normal file
256
src/globals.d.ts
vendored
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
/* eslint-disable typescript/interface-name-prefix */
|
||||||
|
/** Is the app running in dev mode? */
|
||||||
|
declare const __DEV__: boolean
|
||||||
|
|
||||||
|
/** The OAuth client id the app should use */
|
||||||
|
declare const __OAUTH_CLIENT_ID__: string | undefined
|
||||||
|
|
||||||
|
/** The OAuth secret the app should use. */
|
||||||
|
declare const __OAUTH_SECRET__: string | undefined
|
||||||
|
|
||||||
|
/** Is the app being built to run on Darwin? */
|
||||||
|
declare const __DARWIN__: boolean
|
||||||
|
|
||||||
|
/** Is the app being built to run on Win32? */
|
||||||
|
declare const __WIN32__: boolean
|
||||||
|
|
||||||
|
/** Is the app being built to run on Linux? */
|
||||||
|
declare const __LINUX__: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The commit id of the repository HEAD at build time.
|
||||||
|
* Represented as a 40 character SHA-1 hexadecimal digest string.
|
||||||
|
*/
|
||||||
|
declare const __SHA__: string
|
||||||
|
|
||||||
|
/** The channel for which the release was created. */
|
||||||
|
declare const __RELEASE_CHANNEL__:
|
||||||
|
| 'production'
|
||||||
|
| 'beta'
|
||||||
|
| 'test'
|
||||||
|
| 'development'
|
||||||
|
|
||||||
|
declare const __CLI_COMMANDS__: ReadonlyArray<string>
|
||||||
|
|
||||||
|
/** The URL for Squirrel's updates. */
|
||||||
|
declare const __UPDATES_URL__: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently executing process kind, this is specific to desktop
|
||||||
|
* and identifies the processes that we have.
|
||||||
|
*/
|
||||||
|
declare const __PROCESS_KIND__:
|
||||||
|
| 'main'
|
||||||
|
| 'ui'
|
||||||
|
| 'crash'
|
||||||
|
| 'askpass'
|
||||||
|
| 'highlighter'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DOMHighResTimeStamp type is a double and is used to store a time value.
|
||||||
|
*
|
||||||
|
* The value could be a discrete point in time or the difference in time between
|
||||||
|
* two discrete points in time. The unit is milliseconds and should be accurate
|
||||||
|
* to 5 µs (microseconds). However, if the browser is unable to provide a time
|
||||||
|
* value accurate to 5 microseconds (due, for example, to hardware or software
|
||||||
|
* constraints), the browser can represent the value as a time in milliseconds
|
||||||
|
* accurate to a millisecond.
|
||||||
|
*
|
||||||
|
* See https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp
|
||||||
|
*/
|
||||||
|
declare type DOMHighResTimeStamp = number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IdleDeadline interface is used as the data type of the input parameter to
|
||||||
|
* idle callbacks established by calling Window.requestIdleCallback(). It offers
|
||||||
|
* a method, timeRemaining(), which lets you determine how much longer the user
|
||||||
|
* agent estimates it will remain idle and a property, didTimeout, which lets
|
||||||
|
* you determine if your callback is executing because its timeout duration
|
||||||
|
* expired.
|
||||||
|
*
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
|
||||||
|
*/
|
||||||
|
interface IdleDeadline {
|
||||||
|
readonly didTimeout: boolean
|
||||||
|
readonly timeRemaining: () => DOMHighResTimeStamp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains optional configuration parameters for the requestIdleCallback
|
||||||
|
* function.
|
||||||
|
*
|
||||||
|
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
||||||
|
*/
|
||||||
|
interface IdleCallbackOptions {
|
||||||
|
/**
|
||||||
|
* If timeout is specified and has a positive value, and the callback has not
|
||||||
|
* already been called by the time timeout milliseconds have passed, the
|
||||||
|
* timeout will be called during the next idle period, even if doing so risks
|
||||||
|
* causing a negative performance impact..
|
||||||
|
*/
|
||||||
|
readonly timeout: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The window.requestIdleCallback() method queues a function to be called during
|
||||||
|
* a browser's idle periods. This enables developers to perform background and
|
||||||
|
* low priority work on the main event loop, without impacting latency-critical
|
||||||
|
* events such as animation and input response. Functions are generally called
|
||||||
|
* in first-in-first-out order; however, callbacks which have a timeout
|
||||||
|
* specified may be called out-of-order if necessary in order to run them before
|
||||||
|
* the timeout elapses.
|
||||||
|
*
|
||||||
|
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
||||||
|
*
|
||||||
|
* @param options Contains optional configuration parameters. Currently only one
|
||||||
|
* property is defined:
|
||||||
|
* timeout:
|
||||||
|
*/
|
||||||
|
declare function requestIdleCallback(
|
||||||
|
fn: (deadline: IdleDeadline) => void,
|
||||||
|
options?: IdleCallbackOptions
|
||||||
|
): number
|
||||||
|
|
||||||
|
interface IDesktopLogger {
|
||||||
|
/**
|
||||||
|
* Writes a log message at the 'error' level.
|
||||||
|
*
|
||||||
|
* The error will be persisted to disk as long as the disk transport is
|
||||||
|
* configured to pass along log messages at this level. For more details
|
||||||
|
* about the on-disk transport, see log.ts in the main process.
|
||||||
|
*
|
||||||
|
* If used from a renderer the log message will also be appended to the
|
||||||
|
* devtools console.
|
||||||
|
*
|
||||||
|
* @param message The text to write to the log file
|
||||||
|
* @param error An optional error instance that will be formatted to
|
||||||
|
* include the stack trace (if one is available) and
|
||||||
|
* then appended to the log message.
|
||||||
|
*/
|
||||||
|
error(message: string, error?: Error): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a log message at the 'warn' level.
|
||||||
|
*
|
||||||
|
* The error will be persisted to disk as long as the disk transport is
|
||||||
|
* configured to pass along log messages at this level. For more details
|
||||||
|
* about the on-disk transport, see log.ts in the main process.
|
||||||
|
*
|
||||||
|
* If used from a renderer the log message will also be appended to the
|
||||||
|
* devtools console.
|
||||||
|
*
|
||||||
|
* @param message The text to write to the log file
|
||||||
|
* @param error An optional error instance that will be formatted to
|
||||||
|
* include the stack trace (if one is available) and
|
||||||
|
* then appended to the log message.
|
||||||
|
*/
|
||||||
|
warn(message: string, error?: Error): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a log message at the 'info' level.
|
||||||
|
*
|
||||||
|
* The error will be persisted to disk as long as the disk transport is
|
||||||
|
* configured to pass along log messages at this level. For more details
|
||||||
|
* about the on-disk transport, see log.ts in the main process.
|
||||||
|
*
|
||||||
|
* If used from a renderer the log message will also be appended to the
|
||||||
|
* devtools console.
|
||||||
|
*
|
||||||
|
* @param message The text to write to the log file
|
||||||
|
* @param error An optional error instance that will be formatted to
|
||||||
|
* include the stack trace (if one is available) and
|
||||||
|
* then appended to the log message.
|
||||||
|
*/
|
||||||
|
info(message: string, error?: Error): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a log message at the 'debug' level.
|
||||||
|
*
|
||||||
|
* The error will be persisted to disk as long as the disk transport is
|
||||||
|
* configured to pass along log messages at this level. For more details
|
||||||
|
* about the on-disk transport, see log.ts in the main process.
|
||||||
|
*
|
||||||
|
* If used from a renderer the log message will also be appended to the
|
||||||
|
* devtools console.
|
||||||
|
*
|
||||||
|
* @param message The text to write to the log file
|
||||||
|
* @param error An optional error instance that will be formatted to
|
||||||
|
* include the stack trace (if one is available) and
|
||||||
|
* then appended to the log message.
|
||||||
|
*/
|
||||||
|
debug(message: string, error?: Error): void
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const log: IDesktopLogger
|
||||||
|
// these changes should be pushed into the Electron declarations
|
||||||
|
|
||||||
|
declare namespace NodeJS {
|
||||||
|
interface Process extends EventEmitter {
|
||||||
|
once(event: 'uncaughtException', listener: (error: Error) => void): this
|
||||||
|
on(event: 'uncaughtException', listener: (error: Error) => void): this
|
||||||
|
removeListener(event: 'exit', listener: Function): this
|
||||||
|
once(event: 'exit', listener: Function): this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare namespace Electron {
|
||||||
|
interface MenuItem {
|
||||||
|
readonly accelerator?: Electron.Accelerator
|
||||||
|
readonly submenu?: Electron.Menu
|
||||||
|
readonly role?: string
|
||||||
|
readonly type: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestOptions {
|
||||||
|
readonly method: string
|
||||||
|
readonly url: string
|
||||||
|
readonly headers: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppleActionOnDoubleClickPref = 'Maximize' | 'Minimize' | 'None'
|
||||||
|
|
||||||
|
interface SystemPreferences {
|
||||||
|
getUserDefault(
|
||||||
|
key: 'AppleActionOnDoubleClick',
|
||||||
|
type: 'string'
|
||||||
|
): AppleActionOnDoubleClickPref
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebviewTag extends HTMLElement {
|
||||||
|
// Copied from https://github.com/electron/electron-typescript-definitions/pull/81
|
||||||
|
// until we can upgrade to a version of Electron which includes the fix.
|
||||||
|
addEventListener<K extends keyof HTMLElementEventMap>(
|
||||||
|
type: K,
|
||||||
|
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
|
||||||
|
useCapture?: boolean
|
||||||
|
): void
|
||||||
|
addEventListener(
|
||||||
|
type: string,
|
||||||
|
listener: EventListenerOrEventListenerObject,
|
||||||
|
useCapture?: boolean
|
||||||
|
): void
|
||||||
|
removeEventListener<K extends keyof HTMLElementEventMap>(
|
||||||
|
type: K,
|
||||||
|
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
|
||||||
|
useCapture?: boolean
|
||||||
|
): void
|
||||||
|
removeEventListener(
|
||||||
|
type: string,
|
||||||
|
listener: EventListenerOrEventListenerObject,
|
||||||
|
useCapture?: boolean
|
||||||
|
): void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://wicg.github.io/ResizeObserver/#resizeobserverentry
|
||||||
|
interface IResizeObserverEntry {
|
||||||
|
readonly target: HTMLElement
|
||||||
|
readonly contentRect: ClientRect
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ResizeObserver {
|
||||||
|
public constructor(cb: (entries: ReadonlyArray<IResizeObserverEntry>) => void)
|
||||||
|
|
||||||
|
public disconnect(): void
|
||||||
|
public observe(e: HTMLElement): void
|
||||||
|
}
|
|
@ -5,11 +5,11 @@
|
||||||
"outDir": "./dist/out-tsc",
|
"outDir": "./dist/out-tsc",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"module": "es2015",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"target": "es5",
|
"target": "es2017",
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"node_modules/@types"
|
"node_modules/@types"
|
||||||
],
|
],
|
||||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -2202,6 +2202,14 @@ electron-to-chromium@^1.3.47:
|
||||||
version "1.3.58"
|
version "1.3.58"
|
||||||
resolved "https://nexus.loafle.net/repository/npm-all/electron-to-chromium/-/electron-to-chromium-1.3.58.tgz#8267a4000014e93986d9d18c65a8b4022ca75188"
|
resolved "https://nexus.loafle.net/repository/npm-all/electron-to-chromium/-/electron-to-chromium-1.3.58.tgz#8267a4000014e93986d9d18c65a8b4022ca75188"
|
||||||
|
|
||||||
|
electron-window-state@^4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://nexus.loafle.net/repository/npm-all/electron-window-state/-/electron-window-state-4.1.1.tgz#6b34fdc31b38514dfec8b7c8f7b5d4addb67632d"
|
||||||
|
dependencies:
|
||||||
|
deep-equal "^1.0.1"
|
||||||
|
jsonfile "^2.2.3"
|
||||||
|
mkdirp "^0.5.1"
|
||||||
|
|
||||||
electron@^2.0.7:
|
electron@^2.0.7:
|
||||||
version "2.0.7"
|
version "2.0.7"
|
||||||
resolved "https://nexus.loafle.net/repository/npm-all/electron/-/electron-2.0.7.tgz#f7ce410433298e319032ce31f0e6ffd709ff052c"
|
resolved "https://nexus.loafle.net/repository/npm-all/electron/-/electron-2.0.7.tgz#f7ce410433298e319032ce31f0e6ffd709ff052c"
|
||||||
|
@ -2649,6 +2657,14 @@ file-loader@^1.1.11:
|
||||||
loader-utils "^1.0.2"
|
loader-utils "^1.0.2"
|
||||||
schema-utils "^0.4.5"
|
schema-utils "^0.4.5"
|
||||||
|
|
||||||
|
file-uri-to-path@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://nexus.loafle.net/repository/npm-all/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||||
|
|
||||||
|
file-url@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://nexus.loafle.net/repository/npm-all/file-url/-/file-url-2.0.2.tgz#e951784d79095127d3713029ab063f40818ca2ae"
|
||||||
|
|
||||||
filename-regex@^2.0.0:
|
filename-regex@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://nexus.loafle.net/repository/npm-all/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
|
resolved "https://nexus.loafle.net/repository/npm-all/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
|
||||||
|
@ -3938,7 +3954,7 @@ json5@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
|
|
||||||
jsonfile@^2.1.0:
|
jsonfile@^2.1.0, jsonfile@^2.2.3:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://nexus.loafle.net/repository/npm-all/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
|
resolved "https://nexus.loafle.net/repository/npm-all/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
@ -6257,7 +6273,7 @@ source-map-resolve@^0.5.0:
|
||||||
source-map-url "^0.4.0"
|
source-map-url "^0.4.0"
|
||||||
urix "^0.1.0"
|
urix "^0.1.0"
|
||||||
|
|
||||||
source-map-support@^0.5.0, source-map-support@^0.5.3, source-map-support@^0.5.5, source-map-support@^0.5.6:
|
source-map-support@^0.5.0, source-map-support@^0.5.3, source-map-support@^0.5.5, source-map-support@^0.5.6, source-map-support@^0.5.8:
|
||||||
version "0.5.8"
|
version "0.5.8"
|
||||||
resolved "https://nexus.loafle.net/repository/npm-all/source-map-support/-/source-map-support-0.5.8.tgz#04f5581713a8a65612d0175fbf3a01f80a162613"
|
resolved "https://nexus.loafle.net/repository/npm-all/source-map-support/-/source-map-support-0.5.8.tgz#04f5581713a8a65612d0175fbf3a01f80a162613"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user