Merge branch 'development' into refactor-remote-app-get-path-to-main-process

This commit is contained in:
Becca 2022-02-03 13:26:26 -05:00
commit eca362baeb
66 changed files with 1331 additions and 1094 deletions

View file

@ -2,7 +2,6 @@ root: true
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
- babel
- react
- json
- jsdoc
@ -100,9 +99,7 @@ rules:
'@typescript-eslint/no-explicit-any': off
'@typescript-eslint/no-inferrable-types': off
'@typescript-eslint/no-empty-function': off
# Babel
babel/no-invalid-this: error
'@typescript-eslint/no-redeclare': error
# React
react/jsx-boolean-value:
@ -114,6 +111,10 @@ rules:
react/jsx-uses-vars: error
react/jsx-uses-react: error
react/no-unused-state: error
react/no-unused-prop-types: error
react/prop-types:
- error
- ignore: ['children']
# JSDoc
jsdoc/check-alignment: error
@ -136,9 +137,9 @@ rules:
###########
curly: error
no-new-wrappers: error
no-redeclare:
- error
- builtinGlobals: true
# We'll use no-redeclare from @typescript/eslint-plugin instead as that
# supports overloads
no-redeclare: off
no-eval: error
no-sync: error
no-var: error

View file

@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node: [14.15.4]
node: [14.17.0]
os: [macos-10.15, windows-2019]
arch: [x64, arm64]
include:

View file

@ -1 +1 @@
14.15.1
14.17.0

2
.nvmrc
View file

@ -1 +1 @@
v14.15.4
v14.17.0

View file

@ -1,2 +1,2 @@
python 3.9.5
nodejs 14.15.1
nodejs 14.17.0

View file

@ -3,7 +3,7 @@
"productName": "GitHub Desktop",
"bundleID": "com.github.GitHubClient",
"companyName": "GitHub, Inc.",
"version": "2.9.6",
"version": "2.9.7-beta1",
"main": "./main.js",
"repository": {
"type": "git",
@ -26,6 +26,7 @@
"codemirror-mode-elixir": "^1.1.2",
"compare-versions": "^3.6.0",
"deep-equal": "^1.0.1",
"desktop-notifications": "^0.1.4",
"desktop-trampoline": "desktop/desktop-trampoline#v0.9.8",
"detect-arm64-translation": "https://github.com/desktop/node-detect-arm64-translation#v1.0.4",
"dexie": "^2.0.0",
@ -41,7 +42,7 @@
"fs-admin": "^0.19.0",
"fs-extra": "^9.0.1",
"fuzzaldrin-plus": "^0.6.0",
"keytar": "^7.7.0",
"keytar": "^7.8.0",
"marked": "^4.0.10",
"mem": "^4.3.0",
"memoize-one": "^4.0.3",

View file

@ -165,5 +165,5 @@ export function enablePullRequestQuickView(): boolean {
/** Should we enable high-signal notifications? */
export function enableHighSignalNotifications(): boolean {
return enableDevelopmentFeatures()
return __DARWIN__ ? enableBetaFeatures() : enableDevelopmentFeatures()
}

View file

@ -0,0 +1,55 @@
import * as path from 'path'
import * as os from 'os'
import { shell } from 'electron'
/**
* Checks all Windows shortcuts created by Squirrel looking for the toast
* activator CLSID needed to handle Windows notifications from the Action Center.
*/
export function findToastActivatorClsid() {
const shortcutPaths = [
path.join(
os.homedir(),
'AppData',
'Roaming',
'Microsoft',
'Windows',
'Start Menu',
'Programs',
'GitHub, Inc',
'GitHub Desktop.lnk'
),
path.join(os.homedir(), 'Desktop', 'GitHub Desktop.lnk'),
]
for (const shortcutPath of shortcutPaths) {
const toastActivatorClsid = findToastActivatorClsidInShorcut(shortcutPath)
if (toastActivatorClsid !== undefined) {
return toastActivatorClsid
}
}
return undefined
}
function findToastActivatorClsidInShorcut(shortcutPath: string) {
try {
const shortcutDetails = shell.readShortcutLink(shortcutPath)
if (
shortcutDetails.toastActivatorClsid === undefined ||
shortcutDetails.toastActivatorClsid === ''
) {
return undefined
}
return shortcutDetails.toastActivatorClsid
} catch (error) {
log.error(
`Error looking for toast activator CLSID in shortcut ${shortcutPath}`,
error
)
return undefined
}
}

View file

@ -20,7 +20,7 @@ export interface IMatch<T> {
export type KeyFunction<T> = (item: T) => ReadonlyArray<string>
export function match<T, _K extends keyof T>(
export function match<T>(
query: string,
items: ReadonlyArray<T>,
getKey: KeyFunction<T>

View file

@ -55,57 +55,6 @@ declare const __UPDATES_URL__: string
*/
declare const __PROCESS_KIND__: 'main' | 'ui' | 'crash' | 'highlighter'
/**
* 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.
@ -181,9 +130,6 @@ declare const log: IDesktopLogger
declare namespace NodeJS {
interface Process extends EventEmitter {
once(event: 'exit', listener: Function): this
once(event: 'uncaughtException', listener: (error: Error) => void): this
on(event: 'uncaughtException', listener: (error: Error) => void): this
on(
event: 'send-non-fatal-exception',
listener: (error: Error, context?: { [key: string]: string }) => void
@ -197,21 +143,7 @@ declare namespace NodeJS {
}
}
interface XMLHttpRequest extends XMLHttpRequestEventTarget {
/**
* Initiates the request. The optional argument provides the request body. The argument is ignored if request method is GET or HEAD.
* Throws an "InvalidStateError" DOMException if either state is not opened or the send() flag is set.
*/
send(body?: Document | BodyInit | null): void
}
declare namespace Electron {
interface RequestOptions {
readonly method: string
readonly url: string
readonly headers: any
}
type AppleActionOnDoubleClickPref = 'Maximize' | 'Minimize' | 'None'
interface SystemPreferences {
@ -222,19 +154,6 @@ declare namespace Electron {
}
}
// 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
}
declare module 'file-metadata' {
// eslint-disable-next-line no-restricted-syntax
function fileMetadata(path: string): Promise<plist.PlistObject>
@ -246,6 +165,9 @@ interface Window {
Element: typeof Element
}
interface HTMLDialogElement {
showModal: () => void
}
/**
* Obtain the number of elements of a tuple type
*

View file

@ -85,10 +85,12 @@ export type RequestChannels = {
export type RequestResponseChannels = {
'get-path': (path: PathType) => Promise<string>
'get-app-architecture': () => Promise<Architecture>
'get-app-path': () => Promise<string>
'is-running-under-rosetta-translation': () => Promise<boolean>
'move-to-trash': (path: string) => Promise<void>
'show-contextual-menu': (
items: ReadonlyArray<ISerializableMenuItem>
items: ReadonlyArray<ISerializableMenuItem>,
addSpellCheckMenu: boolean
) => Promise<ReadonlyArray<number> | null>
'is-window-focused': () => Promise<boolean>
'open-external': (path: string) => Promise<boolean>

View file

@ -1,3 +1,5 @@
import { invokeContextualMenu } from '../ui/main-process-proxy'
export interface IMenuItem {
/** The user-facing label. */
readonly label?: string
@ -72,3 +74,58 @@ export function getPlatformSpecificNameOrSymbolForModifier(
// Not a known modifier, likely a normal key
return modifier
}
/** Show the given menu items in a contextual menu. */
export async function showContextualMenu(
items: ReadonlyArray<IMenuItem>,
addSpellCheckMenu = false
) {
const indices = await invokeContextualMenu(
serializeMenuItems(items),
addSpellCheckMenu
)
if (indices !== null) {
const menuItem = findSubmenuItem(items, indices)
if (menuItem !== undefined && menuItem.action !== undefined) {
menuItem.action()
}
}
}
/**
* Remove the menu items properties that can't be serializable in
* order to pass them via IPC.
*/
function serializeMenuItems(
items: ReadonlyArray<IMenuItem>
): ReadonlyArray<ISerializableMenuItem> {
return items.map(item => ({
...item,
action: undefined,
submenu: item.submenu ? serializeMenuItems(item.submenu) : undefined,
}))
}
/**
* Traverse the submenus of the context menu until we find the appropriate index.
*/
function findSubmenuItem(
currentContextualMenuItems: ReadonlyArray<IMenuItem>,
indices: ReadonlyArray<number>
): IMenuItem | undefined {
let foundMenuItem: IMenuItem | undefined = {
submenu: currentContextualMenuItems,
}
for (const index of indices) {
if (foundMenuItem === undefined || foundMenuItem.submenu === undefined) {
return undefined
}
foundMenuItem = foundMenuItem.submenu[index]
}
return foundMenuItem
}

View file

@ -23,9 +23,6 @@ let oauthState: IOAuthState | null = null
* flow.
*/
export function askUserToOAuth(endpoint: string) {
// Disable the lint warning since we're storing the `resolve` and `reject`
// functions.
// tslint:disable-next-line:promise-must-complete
return new Promise<Account>((resolve, reject) => {
oauthState = { state: uuid(), endpoint, resolve, reject }

View file

@ -90,8 +90,8 @@ export async function getChangeLog(
'https://central.github.com/deployments/desktop/desktop/changelog.json'
)
if (__RELEASE_CHANNEL__ === 'beta') {
changelogURL.searchParams.set('env', 'beta')
if (__RELEASE_CHANNEL__ === 'beta' || __RELEASE_CHANNEL__ === 'test') {
changelogURL.searchParams.set('env', __RELEASE_CHANNEL__)
}
if (limit !== undefined) {

View file

@ -130,7 +130,7 @@ export class ApiRepositoriesStore extends BaseStore {
this.emitUpdate()
}
private updateAccount<T, K extends keyof IAccountRepositories>(
private updateAccount<K extends keyof IAccountRepositories>(
account: Account,
repositories: Pick<IAccountRepositories, K>
) {

View file

@ -739,8 +739,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
}
/** Load the emoji from disk. */
public loadEmoji() {
const rootDir = getAppPath()
public async loadEmoji() {
const rootDir = await getAppPath()
readEmoji(rootDir)
.then(emoji => {
this.emoji = emoji
@ -5205,7 +5205,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
* resolve when `_completeOpenInDesktop` is called.
*/
public _startOpenInDesktop(fn: () => void): Promise<Repository | null> {
// tslint:disable-next-line:promise-must-complete
const p = new Promise<Repository | null>(
resolve => (this.resolveOpenInDesktop = resolve)
)

View file

@ -140,8 +140,6 @@ export class RepositoryIndicatorUpdater {
public pause() {
if (this.paused === false) {
// Disable the lint warning since we're storing the `resolve`
// tslint:disable-next-line:promise-must-complete
this.pausePromise = new Promise<void>(resolve => {
this.resolvePausePromise = resolve
})

View file

@ -1,4 +1,32 @@
import { focusWindow } from '../../../ui/main-process-proxy'
import {
DesktopNotification,
initializeNotifications,
} from 'desktop-notifications'
import { findToastActivatorClsid } from '../../find-toast-activator-clsid'
let windowsToastActivatorClsid: string | undefined = undefined
function initializeWindowsNotifications() {
if (windowsToastActivatorClsid !== undefined) {
return
}
windowsToastActivatorClsid = findToastActivatorClsid()
if (windowsToastActivatorClsid === undefined) {
log.error(
'Toast activator CLSID not found in any of the shortucts. Falling back to known CLSIDs.'
)
// This is generated by Squirrel.Windows here:
// https://github.com/Squirrel/Squirrel.Windows/blob/7396fa50ccebf97e28c79ef519c07cb9eee121be/src/Squirrel/UpdateManager.ApplyReleases.cs#L258
windowsToastActivatorClsid = '{27D44D0C-A542-5B90-BCDB-AC3126048BA2}'
}
log.info(`Using toast activator CLSID ${windowsToastActivatorClsid}`)
initializeNotifications(windowsToastActivatorClsid)
}
/**
* Shows a notification with a title, a body, and a function to handle when the
@ -9,6 +37,18 @@ export function showNotification(
body: string,
onClick: () => void
) {
if (__WIN32__) {
initializeWindowsNotifications()
const notification = new DesktopNotification(title, body)
notification.onclick = () => {
focusWindow()
onClick()
}
notification.show()
return
}
const notification = new Notification(title, {
body,
})

View file

@ -35,14 +35,6 @@ export async function reportError(
}
}
const requestOptions: Electron.RequestOptions = {
method: 'POST',
url: nonFatal ? NonFatalErrorEndpoint : ErrorEndpoint,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
const body = [...data.entries()]
.map(
([key, value]) =>
@ -52,7 +44,10 @@ export async function reportError(
try {
await new Promise<void>((resolve, reject) => {
const request = net.request(requestOptions)
const url = nonFatal ? NonFatalErrorEndpoint : ErrorEndpoint
const request = net.request({ method: 'POST', url })
request.setHeader('Content-Type', 'application/x-www-form-urlencoded')
request.on('response', response => {
if (response.statusCode === 200) {

View file

@ -37,6 +37,7 @@ import { installSameOriginFilter } from './same-origin-filter'
import * as ipcMain from './ipc-main'
import { getArchitecture } from '../lib/get-architecture'
import * as remoteMain from '@electron/remote/main'
import { buildSpellCheckMenu } from './menu/build-spell-check-menu'
remoteMain.initialize()
app.setAppLogsPath()
@ -450,14 +451,24 @@ app.on('ready', () => {
* Handle the action to show a contextual menu.
*
* It responds an array of indices that maps to the path to reach
* the menu (or submenu) item that was clicked or null if the menu
* was closed without clicking on any item.
* the menu (or submenu) item that was clicked or null if the menu was closed
* without clicking on any item or the item click was handled by the main
* process as opposed to the renderer.
*/
ipcMain.handle('show-contextual-menu', (event, items) => {
return new Promise(resolve => {
const menu = buildContextMenu(items, indices => resolve(indices))
ipcMain.handle('show-contextual-menu', (event, items, addSpellCheckMenu) => {
return new Promise(async resolve => {
const window = BrowserWindow.fromWebContents(event.sender) || undefined
const spellCheckMenuItems = addSpellCheckMenu
? await buildSpellCheckMenu(window)
: undefined
const menu = buildContextMenu(
items,
indices => resolve(indices),
spellCheckMenuItems
)
menu.popup({ window, callback: () => resolve(null) })
})
})
@ -546,6 +557,11 @@ app.on('ready', () => {
*/
ipcMain.handle('get-app-architecture', async () => getArchitecture(app))
/**
* An event sent by the renderer asking for the app's path
*/
ipcMain.handle('get-app-path', async () => app.getAppPath())
/**
* An event sent by the renderer asking for whether the app is running under
* rosetta translation

View file

@ -48,9 +48,20 @@ function getEditMenuItems(): ReadonlyArray<MenuItem> {
*/
export function buildContextMenu(
template: ReadonlyArray<ISerializableMenuItem>,
onClick: (indices: ReadonlyArray<number>) => void
onClick: (indices: ReadonlyArray<number>) => void,
spellCheckMenuItems?: ReadonlyArray<MenuItem>
): Menu {
return buildRecursiveContextMenu(template, onClick)
const menu = buildRecursiveContextMenu(template, onClick)
if (spellCheckMenuItems === undefined) {
return menu
}
for (const spellCheckMenuItem of spellCheckMenuItems) {
menu.append(spellCheckMenuItem)
}
return menu
}
function buildRecursiveContextMenu(

View file

@ -0,0 +1,111 @@
import { app, BrowserWindow, MenuItem } from 'electron'
export async function buildSpellCheckMenu(
window: BrowserWindow | undefined
): Promise<ReadonlyArray<MenuItem> | undefined> {
if (window === undefined) {
return
}
/*
When a user right clicks on a misspelled word in an input, we get event from
electron. That event comes after the context menu event that we get from the
dom.
*/
return new Promise(resolve => {
window.webContents.once('context-menu', (event, params) =>
resolve(getSpellCheckMenuItems(event, params, window.webContents))
)
})
}
function getSpellCheckMenuItems(
event: Electron.Event,
params: Electron.ContextMenuParams,
webContents: Electron.WebContents
): ReadonlyArray<MenuItem> | undefined {
const { misspelledWord, dictionarySuggestions } = params
if (!misspelledWord && dictionarySuggestions.length === 0) {
return
}
const items = new Array<MenuItem>()
items.push(
new MenuItem({
type: 'separator',
})
)
for (const suggestion of dictionarySuggestions) {
items.push(
new MenuItem({
label: suggestion,
click: () => webContents.replaceMisspelling(suggestion),
})
)
}
if (misspelledWord) {
items.push(
new MenuItem({
label: __DARWIN__ ? 'Add to Dictionary' : 'Add to dictionary',
click: () =>
webContents.session.addWordToSpellCheckerDictionary(misspelledWord),
})
)
}
if (!__DARWIN__) {
// NOTE: "On macOS as we use the native APIs there is no way to set the
// language that the spellchecker uses" -- electron docs Therefore, we are
// only allowing setting to English for non-mac machines.
const spellCheckLanguageItem = getSpellCheckLanguageMenuItem(
webContents.session
)
if (spellCheckLanguageItem !== null) {
items.push(spellCheckLanguageItem)
}
}
return items
}
/**
* Method to get a menu item to give user the option to use English or their
* system language.
*
* If system language is english, it returns null. If spellchecker is not set to
* english, it returns item that can set it to English. If spellchecker is set
* to english, it returns the item that can set it to their system language.
*/
function getSpellCheckLanguageMenuItem(
session: Electron.session
): MenuItem | null {
const userLanguageCode = app.getLocale()
const englishLanguageCode = 'en-US'
const spellcheckLanguageCodes = session.getSpellCheckerLanguages()
if (
userLanguageCode === englishLanguageCode &&
spellcheckLanguageCodes.includes(englishLanguageCode)
) {
return null
}
const languageCode =
spellcheckLanguageCodes.includes(englishLanguageCode) &&
!spellcheckLanguageCodes.includes(userLanguageCode)
? userLanguageCode
: englishLanguageCode
const label =
languageCode === englishLanguageCode
? 'Set spellcheck to English'
: 'Set spellcheck to system language'
return new MenuItem({
label,
click: () => session.setSpellCheckerLanguages([languageCode]),
})
}

View file

@ -95,10 +95,7 @@ export class About extends React.Component<IAboutProps, IAboutState> {
}
private renderUpdateButton() {
if (
__RELEASE_CHANNEL__ === 'development' ||
__RELEASE_CHANNEL__ === 'test'
) {
if (__RELEASE_CHANNEL__ === 'development') {
return null
}
@ -180,14 +177,11 @@ export class About extends React.Component<IAboutProps, IAboutState> {
return null
}
if (
__RELEASE_CHANNEL__ === 'development' ||
__RELEASE_CHANNEL__ === 'test'
) {
if (__RELEASE_CHANNEL__ === 'development') {
return (
<p>
The application is currently running in development or test mode and
will not receive any updates.
The application is currently running in development and will not
receive any updates.
</p>
)
}
@ -216,10 +210,7 @@ export class About extends React.Component<IAboutProps, IAboutState> {
return null
}
if (
__RELEASE_CHANNEL__ === 'development' ||
__RELEASE_CHANNEL__ === 'test'
) {
if (__RELEASE_CHANNEL__ === 'development') {
return null
}

View file

@ -42,8 +42,8 @@ export class Acknowledgements extends React.Component<
this.state = { licenses: null }
}
public componentDidMount() {
const path = Path.join(getAppPath(), 'static', 'licenses.json')
public async componentDidMount() {
const path = Path.join(await getAppPath(), 'static', 'licenses.json')
Fs.readFile(path, 'utf8', (err, data) => {
if (err) {
log.error('Error loading licenses', err)

View file

@ -253,10 +253,7 @@ export class App extends React.Component<IAppProps, IAppState> {
const status = state.status
if (
!(
__RELEASE_CHANNEL__ === 'development' ||
__RELEASE_CHANNEL__ === 'test'
) &&
!(__RELEASE_CHANNEL__ === 'development') &&
status === UpdateStatus.UpdateReady
) {
this.props.dispatcher.setUpdateBannerVisibility(true)
@ -302,8 +299,14 @@ export class App extends React.Component<IAppProps, IAppState> {
this.props.dispatcher.installGlobalLFSFilters(false)
setInterval(() => this.checkForUpdates(true), UpdateCheckInterval)
this.checkForUpdates(true)
// We only want to automatically check for updates on beta and prod
if (
__RELEASE_CHANNEL__ !== 'development' &&
__RELEASE_CHANNEL__ !== 'test'
) {
setInterval(() => this.checkForUpdates(true), UpdateCheckInterval)
this.checkForUpdates(true)
}
log.info(`launching: ${getVersion()} (${getOS()})`)
log.info(`execPath: '${process.execPath}'`)
@ -555,14 +558,7 @@ export class App extends React.Component<IAppProps, IAppState> {
}
private checkForUpdates(inBackground: boolean) {
if (__LINUX__) {
return
}
if (
__RELEASE_CHANNEL__ === 'development' ||
__RELEASE_CHANNEL__ === 'test'
) {
if (__LINUX__ || __RELEASE_CHANNEL__ === 'development') {
return
}

View file

@ -15,7 +15,7 @@ interface IRange {
}
import getCaretCoordinates from 'textarea-caret'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
interface IAutocompletingTextInputProps<ElementType> {
/**

View file

@ -6,7 +6,7 @@ import { IMatches } from '../../lib/fuzzy-find'
import { Octicon } from '../octicons'
import * as OcticonSymbol from '../octicons/octicons.generated'
import { HighlightText } from '../lib/highlight-text'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { IMenuItem } from '../../lib/menu-item'
import { dragAndDropManager } from '../../lib/drag-and-drop-manager'
import { DragType, DropTargetType } from '../../models/drag-drop'

View file

@ -27,7 +27,7 @@ import {
import { CommitMessage } from './commit-message'
import { ChangedFile } from './changed-file'
import { IAutocompletionProvider } from '../autocompletion'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { arrayEquals } from '../../lib/equality'
import { clipboard } from 'electron'
import { basename } from 'path'

View file

@ -27,7 +27,7 @@ import { CommitWarning, CommitWarningIcon } from './commit-warning'
import { LinkButton } from '../lib/link-button'
import { FoldoutType } from '../../lib/app-state'
import { IAvatarUser, getAvatarUserFromAuthor } from '../../models/avatar'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { Account } from '../../models/account'
import { CommitMessageAvatar } from './commit-message-avatar'
import { getDotComAPIEndpoint } from '../../lib/api'

View file

@ -69,8 +69,8 @@ export class ModifiedImageDiff extends React.Component<
super(props)
this.resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
if (entry.target === this.container) {
for (const { target, contentRect } of entries) {
if (target === this.container && target instanceof HTMLElement) {
// We might end up causing a recursive update by updating the state
// when we're reacting to a resize so we'll defer it until after
// react is done with this frame.
@ -80,8 +80,8 @@ export class ModifiedImageDiff extends React.Component<
this.resizedTimeoutID = setImmediate(
this.onResized,
entry.target,
entry.contentRect
target,
contentRect
)
}
}

View file

@ -46,7 +46,7 @@ import {
getLineWidthFromDigitCount,
getNumberOfDigits,
} from './diff-helpers'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { getTokens } from './diff-syntax-mode'
import { DiffSearchInput } from './diff-search-input'
import { escapeRegExp } from '../../lib/helpers/regex'

View file

@ -40,7 +40,7 @@ import { structuralEquals } from '../../lib/equality'
import { assertNever } from '../../lib/fatal-error'
import { clamp } from '../../lib/clamp'
import { uuid } from '../../lib/uuid'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { IMenuItem } from '../../lib/menu-item'
import {
canSelect,

View file

@ -6,7 +6,7 @@ import { RichText } from '../lib/rich-text'
import { RelativeTime } from '../relative-time'
import { getDotComAPIEndpoint } from '../../lib/api'
import { clipboard } from 'electron'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { CommitAttribution } from '../lib/commit-attribution'
import { AvatarStack } from '../lib/avatar-stack'
import { IMenuItem } from '../../lib/menu-item'

View file

@ -23,7 +23,7 @@ import { ThrottledScheduler } from '../lib/throttled-scheduler'
import { Dispatcher } from '../dispatcher'
import { Resizable } from '../resizable'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { CommitSummary } from './commit-summary'
import { FileList } from './file-list'

View file

@ -1,9 +1,7 @@
import * as remote from '@electron/remote'
import { getPath } from '../main-process-proxy'
import { getAppPathProxy } from '../main-process-proxy'
let app: Electron.App | null = null
let path: string | null = null
let userDataPath: string | null = null
let documentsPath: string | null = null
export type PathType =
@ -24,14 +22,6 @@ export type PathType =
| 'logs'
| 'crashDumps'
function getApp(): Electron.App {
if (!app) {
app = remote.app
}
return app
}
/**
* Get the version of the app.
*
@ -53,27 +43,14 @@ export function getName(): string {
*
* This is preferable to using `remote` directly because we cache the result.
*/
export function getAppPath(): string {
export async function getAppPath(): Promise<string> {
if (!path) {
path = getApp().getAppPath()
path = await getAppPathProxy()
}
return path
}
/**
* Get the path to the user's data.
*
* This is preferable to using `remote` directly because we cache the result.
*/
export function getUserDataPath(): string {
if (!userDataPath) {
userDataPath = getApp().getPath('userData')
}
return userDataPath
}
/**
* Get the path to the user's documents path.
*

View file

@ -13,7 +13,7 @@ import { arrayEquals } from '../../lib/equality'
import { syncClockwise } from '../octicons'
import * as OcticonSymbol from '../octicons/octicons.generated'
import { IAuthor } from '../../models/author'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { IMenuItem } from '../../lib/menu-item'
import { getLegacyStealthEmailForUser } from '../../lib/email'

View file

@ -10,7 +10,7 @@ import {
import { join } from 'path'
import { Repository } from '../../../models/repository'
import { Dispatcher } from '../../dispatcher'
import { showContextualMenu } from '../../main-process-proxy'
import { showContextualMenu } from '../../../lib/menu-item'
import { Octicon } from '../../octicons'
import * as OcticonSymbol from '../../octicons/octicons.generated'
import { PathText } from '../path-text'

View file

@ -1,8 +1,5 @@
import * as React from 'react'
// broken for SFCs: https://github.com/Microsoft/tslint-microsoft-contrib/issues/339
/* tslint:disable react-unused-props-and-state */
interface IHighlightTextProps {
/** The text to render */
readonly text: string

View file

@ -288,8 +288,8 @@ export class List extends React.Component<IListProps, IListState> {
if (ResizeObserver || false) {
this.resizeObserver = new ResizeObserverClass(entries => {
for (const entry of entries) {
if (entry.target === this.list) {
for (const { target, contentRect } of entries) {
if (target === this.list && this.list !== null) {
// We might end up causing a recursive update by updating the state
// when we're reacting to a resize so we'll defer it until after
// react is done with this frame.
@ -299,8 +299,8 @@ export class List extends React.Component<IListProps, IListState> {
this.updateSizeTimeoutId = setImmediate(
this.onResized,
entry.target,
entry.contentRect
this.list,
contentRect
)
}
}

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import classNames from 'classnames'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
interface ITextAreaProps {
/** The label for the textarea field. */

View file

@ -2,7 +2,7 @@ import * as React from 'react'
import classNames from 'classnames'
import { createUniqueId, releaseUniqueId } from './id-pool'
import { LinkButton } from './link-button'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
export interface ITextBoxProps {
/** The label for the input field. */

View file

@ -1,6 +1,4 @@
import * as remote from '@electron/remote'
import { ExecutableMenuItem } from '../models/app-menu'
import { IMenuItem, ISerializableMenuItem } from '../lib/menu-item'
import { RequestResponseChannels, RequestChannels } from '../lib/ipc-shared'
import * as ipcRenderer from '../lib/ipc-renderer'
@ -184,6 +182,11 @@ export const getPath = invokeProxy('get-path', 1)
*/
export const getAppArchitecture = invokeProxy('get-app-architecture', 0)
/**
* Tell the main process to obtain the application's app path
*/
export const getAppPathProxy = invokeProxy('get-app-path', 0)
/**
* Tell the main process to obtain whether the app is running under a rosetta
* translation
@ -220,176 +223,7 @@ export const moveToApplicationsFolder = sendProxy(
*/
export const getAppMenu = sendProxy('get-app-menu', 0)
function findSubmenuItem(
currentContextualMenuItems: ReadonlyArray<IMenuItem>,
indices: ReadonlyArray<number>
): IMenuItem | undefined {
let foundMenuItem: IMenuItem | undefined = {
submenu: currentContextualMenuItems,
}
// Traverse the submenus of the context menu until we find the appropriate index.
for (const index of indices) {
if (foundMenuItem === undefined || foundMenuItem.submenu === undefined) {
return undefined
}
foundMenuItem = foundMenuItem.submenu[index]
}
return foundMenuItem
}
let deferredContextMenuItems: ReadonlyArray<IMenuItem> | null = null
/** Takes a context menu and spelling suggestions from electron and merges them
* into one context menu. */
function mergeDeferredContextMenuItems(
event: Electron.Event,
params: Electron.ContextMenuParams
) {
if (deferredContextMenuItems === null) {
return
}
const items = [...deferredContextMenuItems]
const { misspelledWord, dictionarySuggestions } = params
if (!misspelledWord && dictionarySuggestions.length === 0) {
showContextualMenu(items, false)
return
}
items.push({ type: 'separator' })
const { webContents } = remote.getCurrentWindow()
for (const suggestion of dictionarySuggestions) {
items.push({
label: suggestion,
action: () => webContents.replaceMisspelling(suggestion),
})
}
if (misspelledWord) {
items.push({
label: __DARWIN__ ? 'Add to Dictionary' : 'Add to dictionary',
action: () =>
webContents.session.addWordToSpellCheckerDictionary(misspelledWord),
})
}
if (!__DARWIN__) {
// NOTE: "On macOS as we use the native APIs there is no way to set the
// language that the spellchecker uses" -- electron docs Therefore, we are
// only allowing setting to English for non-mac machines.
const spellCheckLanguageItem = getSpellCheckLanguageMenuItem(
webContents.session
)
if (spellCheckLanguageItem !== null) {
items.push(spellCheckLanguageItem)
}
}
showContextualMenu(items, false)
}
/**
* Method to get a menu item to give user the option to use English or their
* system language.
*
* If system language is english, it returns null. If spellchecker is not set to
* english, it returns item that can set it to English. If spellchecker is set
* to english, it returns the item that can set it to their system language.
*/
function getSpellCheckLanguageMenuItem(
session: Electron.session
): IMenuItem | null {
const userLanguageCode = remote.app.getLocale()
const englishLanguageCode = 'en-US'
const spellcheckLanguageCodes = session.getSpellCheckerLanguages()
if (
userLanguageCode === englishLanguageCode &&
spellcheckLanguageCodes.includes(englishLanguageCode)
) {
return null
}
const languageCode =
spellcheckLanguageCodes.includes(englishLanguageCode) &&
!spellcheckLanguageCodes.includes(userLanguageCode)
? userLanguageCode
: englishLanguageCode
const label =
languageCode === englishLanguageCode
? 'Set spellcheck to English'
: 'Set spellcheck to system language'
return {
label,
action: () => session.setSpellCheckerLanguages([languageCode]),
}
}
const _showContextualMenu = invokeProxy('show-contextual-menu', 1)
/** Show the given menu items in a contextual menu. */
export async function showContextualMenu(
items: ReadonlyArray<IMenuItem>,
mergeWithSpellcheckSuggestions = false
) {
/*
When a user right clicks on a misspelled word in an input, we get event from
electron. That event comes after the context menu event that we get from the
dom. In order merge the spelling suggestions from electron with the context
menu that the input wants to show, we stash the context menu items from the
input away while we wait for the event from electron.
*/
if (deferredContextMenuItems !== null) {
deferredContextMenuItems = null
remote
.getCurrentWebContents()
.off('context-menu', mergeDeferredContextMenuItems)
}
if (mergeWithSpellcheckSuggestions) {
deferredContextMenuItems = items
remote
.getCurrentWebContents()
.once('context-menu', mergeDeferredContextMenuItems)
return
}
/*
This is a regular context menu that does not need to merge with spellcheck
items. They can be shown right away.
*/
const indices = await _showContextualMenu(serializeMenuItems(items))
if (indices !== null) {
const menuItem = findSubmenuItem(items, indices)
if (menuItem !== undefined && menuItem.action !== undefined) {
menuItem.action()
}
}
}
/**
* Remove the menu items properties that can't be serializable in
* order to pass them via IPC.
*/
function serializeMenuItems(
items: ReadonlyArray<IMenuItem>
): ReadonlyArray<ISerializableMenuItem> {
return items.map(item => ({
...item,
action: undefined,
submenu: item.submenu ? serializeMenuItems(item.submenu) : undefined,
}))
}
export const invokeContextualMenu = invokeProxy('show-contextual-menu', 2)
/** Update the menu item labels with the user's preferred apps. */
export const updatePreferredAppMenuItemLabels = sendProxy(

View file

@ -45,23 +45,13 @@ export interface IMultiCommitOperationProps {
export abstract class BaseMultiCommitOperation extends React.Component<
IMultiCommitOperationProps
> {
protected abstract onBeginOperation = () => {}
protected abstract onChooseBranch = (targetBranch: Branch) => {}
protected abstract onContinueAfterConflicts = async (): Promise<void> => {}
protected abstract onAbort = async (): Promise<void> => {}
protected abstract onConflictsDialogDismissed = () => {}
protected abstract renderChooseBranch = (): JSX.Element | null => {
return null
}
protected abstract renderCreateBranch = (): JSX.Element | null => {
return null
}
protected abstract onBeginOperation: () => void
protected abstract onChooseBranch: (targetBranch: Branch) => void
protected abstract onContinueAfterConflicts: () => Promise<void>
protected abstract onAbort: () => Promise<void>
protected abstract onConflictsDialogDismissed: () => void
protected abstract renderChooseBranch: () => JSX.Element | null
protected abstract renderCreateBranch: () => JSX.Element | null
protected onFlowEnded = () => {
this.props.dispatcher.closePopup(PopupType.MultiCommitOperation)

View file

@ -79,23 +79,17 @@ export abstract class BaseChooseBranchDialog extends React.Component<
IBaseChooseBranchDialogProps,
IBaseChooseBranchDialogState
> {
protected abstract start = () => {}
protected abstract start: () => void
protected abstract canStart = (): boolean => {
return false
}
protected abstract canStart: () => boolean
protected abstract updateStatus = async (branch: Branch) => {}
protected abstract updateStatus: (branch: Branch) => Promise<void>
protected abstract getDialogTitle = (
protected abstract getDialogTitle: (
branchName: string
): string | JSX.Element | undefined => {
return branchName
}
) => string | JSX.Element | undefined
protected abstract renderActionStatusIcon = (): JSX.Element | null => {
return null
}
protected abstract renderActionStatusIcon: () => JSX.Element | null
public constructor(props: IBaseChooseBranchDialogProps) {
super(props)

View file

@ -16,7 +16,7 @@ import { Dispatcher } from '../dispatcher'
import { Button } from '../lib/button'
import { Octicon } from '../octicons'
import * as OcticonSymbol from '../octicons/octicons.generated'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { IMenuItem } from '../../lib/menu-item'
import { PopupType } from '../../models/popup'
import { encodePathAsUrl } from '../../lib/path'

View file

@ -4,7 +4,7 @@ import { clipboard } from 'electron'
import { Repository } from '../../models/repository'
import { Octicon, iconForRepository } from '../octicons'
import * as OcticonSymbol from '../octicons/octicons.generated'
import { showContextualMenu } from '../main-process-proxy'
import { showContextualMenu } from '../../lib/menu-item'
import { Repositoryish } from './group-repositories'
import { IMenuItem } from '../../lib/menu-item'
import { HighlightText } from '../lib/highlight-text'

View file

@ -0,0 +1,68 @@
import * as React from 'react'
import classNames from 'classnames'
import { TabBarType } from './tab-bar-type'
interface ITabBarItemProps {
readonly index: number
readonly selected: boolean
readonly onClick: (index: number) => void
readonly onMouseEnter: (index: number) => void
readonly onMouseLeave: () => void
readonly onSelectAdjacent: (
direction: 'next' | 'previous',
index: number
) => void
readonly onButtonRef: (
index: number,
button: HTMLButtonElement | null
) => void
readonly type?: TabBarType
}
export class TabBarItem extends React.Component<ITabBarItemProps, {}> {
private onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
this.props.onClick(this.props.index)
}
private onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
const { type, index } = this.props
const previousKey = type === TabBarType.Vertical ? 'ArrowUp' : 'ArrowLeft'
const nextKey = type === TabBarType.Vertical ? 'ArrowDown' : 'ArrowRight'
if (event.key === previousKey) {
this.props.onSelectAdjacent('previous', index)
event.preventDefault()
} else if (event.key === nextKey) {
this.props.onSelectAdjacent('next', index)
event.preventDefault()
}
}
private onButtonRef = (buttonRef: HTMLButtonElement | null) => {
this.props.onButtonRef(this.props.index, buttonRef)
}
private onMouseEnter = () => {
this.props.onMouseEnter(this.props.index)
}
public render() {
const selected = this.props.selected
const className = classNames('tab-bar-item', { selected })
return (
<button
ref={this.onButtonRef}
className={className}
onClick={this.onClick}
role="tab"
aria-selected={selected}
tabIndex={selected ? undefined : -1}
onKeyDown={this.onKeyDown}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
type="button"
>
{this.props.children}
</button>
)
}
}

View file

@ -0,0 +1,11 @@
/** The tab bar type. */
export enum TabBarType {
/** Standard tabs */
Tabs,
/** Simpler switch appearance */
Switch,
/** Vertical tabs */
Vertical,
}

View file

@ -1,22 +1,12 @@
import * as React from 'react'
import classNames from 'classnames'
import { dragAndDropManager } from '../lib/drag-and-drop-manager'
import { TabBarItem } from './tab-bar-item'
import { TabBarType } from './tab-bar-type'
export { TabBarType } from './tab-bar-type'
/** Time to wait for drag element hover before switching tabs */
const dragTabSwitchWaitTime = 500
/** The tab bar type. */
export enum TabBarType {
/** Standard tabs */
Tabs,
/** Simpler switch appearance */
Switch,
/** Vertical tabs */
Vertical,
}
interface ITabBarProps {
/** The currently selected tab. */
readonly selectedIndex: number
@ -144,68 +134,3 @@ export class TabBar extends React.Component<ITabBarProps, {}> {
})
}
}
interface ITabBarItemProps {
readonly index: number
readonly selected: boolean
readonly onClick: (index: number) => void
readonly onMouseEnter: (index: number) => void
readonly onMouseLeave: () => void
readonly onSelectAdjacent: (
direction: 'next' | 'previous',
index: number
) => void
readonly onButtonRef: (
index: number,
button: HTMLButtonElement | null
) => void
readonly type?: TabBarType
}
class TabBarItem extends React.Component<ITabBarItemProps, {}> {
private onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
this.props.onClick(this.props.index)
}
private onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
const { type, index } = this.props
const previousKey = type === TabBarType.Vertical ? 'ArrowUp' : 'ArrowLeft'
const nextKey = type === TabBarType.Vertical ? 'ArrowDown' : 'ArrowRight'
if (event.key === previousKey) {
this.props.onSelectAdjacent('previous', index)
event.preventDefault()
} else if (event.key === nextKey) {
this.props.onSelectAdjacent('next', index)
event.preventDefault()
}
}
private onButtonRef = (buttonRef: HTMLButtonElement | null) => {
this.props.onButtonRef(this.props.index, buttonRef)
}
private onMouseEnter = () => {
this.props.onMouseEnter(this.props.index)
}
public render() {
const selected = this.props.selected
const className = classNames('tab-bar-item', { selected })
return (
<button
ref={this.onButtonRef}
className={className}
onClick={this.onClick}
role="tab"
aria-selected={selected}
tabIndex={selected ? undefined : -1}
onKeyDown={this.onKeyDown}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
type="button"
>
{this.props.children}
</button>
)
}
}

View file

@ -16,6 +16,7 @@ import { PopupType } from '../../models/popup'
import { PreferencesTab } from '../../models/preferences'
import { Ref } from '../lib/ref'
import { suggestedExternalEditor } from '../../lib/editors/shared'
import { TutorialStepInstructions } from './tutorial-step-instruction'
const TutorialPanelImage = encodePathAsUrl(
__dirname,
@ -310,93 +311,6 @@ export class TutorialPanel extends React.Component<
}
}
interface ITutorialStepInstructionsProps {
/** Text displayed to summarize this step */
readonly summaryText: string
/** Used to find out if this step has been completed */
readonly isComplete: (step: ValidTutorialStep) => boolean
/** The step for this section */
readonly sectionId: ValidTutorialStep
/** Used to find out if this is the next step for the user to complete */
readonly isNextStepTodo: (step: ValidTutorialStep) => boolean
/** ID of the currently expanded tutorial step
* (used to determine if this step is expanded)
*/
readonly currentlyOpenSectionId: ValidTutorialStep
/** Skip button (if possible for this step) */
readonly skipLinkButton?: JSX.Element
/** Handler to open and close section */
readonly onSummaryClick: (id: ValidTutorialStep) => void
}
/** A step (summary and expandable description) in the tutorial side panel */
class TutorialStepInstructions extends React.Component<
ITutorialStepInstructionsProps
> {
public render() {
return (
<li key={this.props.sectionId} onClick={this.onSummaryClick}>
<details
open={this.props.sectionId === this.props.currentlyOpenSectionId}
onClick={this.onSummaryClick}
>
{this.renderSummary()}
<div className="contents">{this.props.children}</div>
</details>
</li>
)
}
private renderSummary = () => {
const shouldShowSkipLink =
this.props.skipLinkButton !== undefined &&
this.props.currentlyOpenSectionId === this.props.sectionId &&
this.props.isNextStepTodo(this.props.sectionId)
return (
<summary>
{this.renderTutorialStepIcon()}
<span className="summary-text">{this.props.summaryText}</span>
<span className="hang-right">
{shouldShowSkipLink ? (
this.props.skipLinkButton
) : (
<Octicon symbol={OcticonSymbol.chevronDown} />
)}
</span>
</summary>
)
}
private renderTutorialStepIcon() {
if (this.props.isComplete(this.props.sectionId)) {
return (
<div className="green-circle">
<Octicon symbol={OcticonSymbol.check} />
</div>
)
}
// ugh zero-indexing
const stepNumber = orderedTutorialSteps.indexOf(this.props.sectionId) + 1
return this.props.isNextStepTodo(this.props.sectionId) ? (
<div className="blue-circle">{stepNumber}</div>
) : (
<div className="empty-circle">{stepNumber}</div>
)
}
private onSummaryClick = (e: React.MouseEvent<HTMLElement>) => {
// prevents the default behavior of toggling on a `details` html element
// so we don't have to fight it with our react state
// for more info see:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details#Events
e.preventDefault()
this.props.onSummaryClick(this.props.sectionId)
}
}
const SkipLinkButton: React.FunctionComponent<{
onClick: () => void
}> = props => <LinkButton onClick={props.onClick}>Skip</LinkButton>

View file

@ -0,0 +1,94 @@
import * as React from 'react'
import {
ValidTutorialStep,
orderedTutorialSteps,
} from '../../models/tutorial-step'
import { Octicon } from '../octicons'
import * as OcticonSymbol from '../octicons/octicons.generated'
interface ITutorialStepInstructionsProps {
/** Text displayed to summarize this step */
readonly summaryText: string
/** Used to find out if this step has been completed */
readonly isComplete: (step: ValidTutorialStep) => boolean
/** The step for this section */
readonly sectionId: ValidTutorialStep
/** Used to find out if this is the next step for the user to complete */
readonly isNextStepTodo: (step: ValidTutorialStep) => boolean
/** ID of the currently expanded tutorial step
* (used to determine if this step is expanded)
*/
readonly currentlyOpenSectionId: ValidTutorialStep
/** Skip button (if possible for this step) */
readonly skipLinkButton?: JSX.Element
/** Handler to open and close section */
readonly onSummaryClick: (id: ValidTutorialStep) => void
}
/** A step (summary and expandable description) in the tutorial side panel */
export class TutorialStepInstructions extends React.Component<
ITutorialStepInstructionsProps
> {
public render() {
return (
<li key={this.props.sectionId} onClick={this.onSummaryClick}>
<details
open={this.props.sectionId === this.props.currentlyOpenSectionId}
onClick={this.onSummaryClick}
>
{this.renderSummary()}
<div className="contents">{this.props.children}</div>
</details>
</li>
)
}
private renderSummary = () => {
const shouldShowSkipLink =
this.props.skipLinkButton !== undefined &&
this.props.currentlyOpenSectionId === this.props.sectionId &&
this.props.isNextStepTodo(this.props.sectionId)
return (
<summary>
{this.renderTutorialStepIcon()}
<span className="summary-text">{this.props.summaryText}</span>
<span className="hang-right">
{shouldShowSkipLink ? (
this.props.skipLinkButton
) : (
<Octicon symbol={OcticonSymbol.chevronDown} />
)}
</span>
</summary>
)
}
private renderTutorialStepIcon() {
if (this.props.isComplete(this.props.sectionId)) {
return (
<div className="green-circle">
<Octicon symbol={OcticonSymbol.check} />
</div>
)
}
// ugh zero-indexing
const stepNumber = orderedTutorialSteps.indexOf(this.props.sectionId) + 1
return this.props.isNextStepTodo(this.props.sectionId) ? (
<div className="blue-circle">{stepNumber}</div>
) : (
<div className="empty-circle">{stepNumber}</div>
)
}
private onSummaryClick = (e: React.MouseEvent<HTMLElement>) => {
// prevents the default behavior of toggling on a `details` html element
// so we don't have to fight it with our react state
// for more info see:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details#Events
e.preventDefault()
this.props.onSummaryClick(this.props.sectionId)
}
}

View file

@ -3,8 +3,6 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { WindowState } from '../../lib/window-state'
interface IFullScreenInfoProps {
// react-unused-props-and-state doesn't understand getDerivedStateFromProps
// tslint:disable-next-line:react-unused-props-and-state
readonly windowState: WindowState | null
}

View file

@ -2,7 +2,7 @@
# The least terrible way to resolve a symlink to its real path.
function realpath() {
/usr/bin/python -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0";
/usr/bin/perl -e "use Cwd;print Cwd::abs_path(@ARGV[0])" "$0";
}
CONTENTS="$(dirname "$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")")"

View file

@ -33,11 +33,7 @@ const commonConfig: webpack.Configuration = {
include: path.resolve(__dirname, 'src'),
use: [
{
loader: 'awesome-typescript-loader',
options: {
useBabel: false,
useCache: true,
},
loader: 'ts-loader',
},
],
exclude: /node_modules/,
@ -220,14 +216,9 @@ highlighter.module!.rules = [
include: path.resolve(__dirname, 'src/highlighter'),
use: [
{
loader: 'awesome-typescript-loader',
loader: 'ts-loader',
options: {
useBabel: false,
useCache: true,
configFileName: path.resolve(
__dirname,
'src/highlighter/tsconfig.json'
),
configFile: path.resolve(__dirname, 'src/highlighter/tsconfig.json'),
},
},
],

View file

@ -306,6 +306,13 @@ decompress-response@^4.2.0:
dependencies:
mimic-response "^2.0.0"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.0"
deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@ -326,6 +333,15 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
desktop-notifications@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/desktop-notifications/-/desktop-notifications-0.1.4.tgz#698c5822fb88d7a2289f36b4f64eaac8b5bdefed"
integrity sha512-3ADhNvqL9ISbN17x/hWcYUcGHvWJxPnoxxsQbmrU26wpk2eakvDplCm0fvNIpHx/Ar5O7n1VmMOVmLr8W6B8sQ==
dependencies:
node-addon-api "^4.3.0"
prebuild-install "^7.0.1"
uuid "^8.3.2"
desktop-trampoline@desktop/desktop-trampoline#v0.9.8:
version "0.9.8"
resolved "https://codeload.github.com/desktop/desktop-trampoline/tar.gz/cbd3dbb31d0d3ea9f325067f48bfbf60b6663a57"
@ -345,6 +361,11 @@ detect-libc@^1.0.3:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
detect-libc@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.0.tgz#c528bc09bc6d1aa30149228240917c225448f204"
integrity sha512-S55LzUl8HUav8l9E2PBTlC5PAJrHK7tkM+XXFGD+fbsbkTzhCpG6K05LxJcUOEWzMa4v6ptcMZ9s3fOdJDu0Zw==
devtron@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/devtron/-/devtron-1.4.0.tgz#b5e748bd6e95bbe70bfcc68aae6fe696119441e1"
@ -832,13 +853,13 @@ keyboardevents-areequal@^0.2.1:
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
keytar@^7.7.0:
version "7.7.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.7.0.tgz#3002b106c01631aa79b1aa9ee0493b94179bbbd2"
integrity sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==
keytar@^7.8.0:
version "7.8.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.8.0.tgz#28cf5ceeb1275350888870022869b8b4fe6a87f9"
integrity sha512-mR+BqtAOIW8j+T5FtLVyckCbvROWQD+4FzPeFMuk5njEZkXLpVPCGF26Y3mTyxMAAL1XCfswR7S6kIf+THSRFA==
dependencies:
node-addon-api "^3.0.0"
prebuild-install "^6.0.0"
node-addon-api "^4.3.0"
prebuild-install "^7.0.1"
keyv@^3.0.0:
version "3.1.0"
@ -888,6 +909,13 @@ lowercase-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
map-age-cleaner@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
@ -934,6 +962,11 @@ mimic-response@^2.0.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46"
integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@ -1027,16 +1060,23 @@ node-abi@^2.7.0:
dependencies:
semver "^5.4.1"
node-addon-api@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681"
integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==
node-abi@^3.3.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.7.0.tgz#ed980f6dbb6db9ff3b31aeb27d43cd9b096f6e9e"
integrity sha512-3J+U4CvxVNEk9+lGdJkmYbN8cIN0HMTDT9R0ezX7pmp7aD6BaKsfAHwVn3IvVg6pYIRUuQ+gHW1eawrvywnSQQ==
dependencies:
semver "^7.3.5"
node-addon-api@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
node-addon-api@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -1199,6 +1239,25 @@ prebuild-install@^6.0.0:
tunnel-agent "^0.6.0"
which-pm-runs "^1.0.0"
prebuild-install@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870"
integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg==
dependencies:
detect-libc "^2.0.0"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^3.3.0"
npmlog "^4.0.1"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^4.0.0"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
prepend-http@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
@ -1466,6 +1525,13 @@ semver@^7.2.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@ -1512,6 +1578,15 @@ simple-get@^3.0.3:
once "^1.3.1"
simple-concat "^1.0.0"
simple-get@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
dependencies:
decompress-response "^6.0.0"
once "^1.3.1"
simple-concat "^1.0.0"
source-map-support@^0.4.15:
version "0.4.18"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
@ -1719,6 +1794,11 @@ uuid@^3.0.1:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
warning@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
@ -1816,3 +1896,8 @@ yallist@^3.0.0, yallist@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==

View file

@ -1,5 +1,21 @@
{
"releases": {
"2.9.7-beta1": [
"[New] Initial support for system notifications when checks fail in macOS - #13655",
"[Added] Support pushing workflow files for GitHub Actions to GitHub Enterprise Server - #13640",
"[Added] Support CLion as an external editor - #13739. Thanks @Pinzauti!",
"[Fixed] Fix close button in full screen mode on macOS - #12838",
"[Fixed] Commit message dialog background styles match dialog - #13606",
"[Fixed] Ensure job steps on pull request check run list are always present - #13531",
"[Improved] Take alias into account when sorting repositories - #13429",
"[Improved] Upgrade to Electron v14.2.3 - #13689",
"[Improved] Support avatars on GitHub Enterprise Server - #13719",
"[Improved] Fetch before trying to follow a URL link to a specific branch - #13641. Thanks @Bestra!",
"[Improved] Add \"View on GitHub\" context menu option to repository list items - #13227. Thanks @lhvy!",
"[Improved] Signal when a commit summary is getting long - #2055. Thanks @Twixes!",
"[Improved] Check run group headers and checks stay in view while scrolling the sub checks or job steps. - #13532",
"[Improved] Remove unnecessary punctuation in appearance settings- #13715. Thanks @Pinzauti!"
],
"2.9.6": [
"[Added] View and re-run the check runs for the checked out pull request.",
"[Fixed] Tooltip improvements and polish - #13452 #13449",

View file

@ -8,7 +8,6 @@ Our linting tooling uses a combination of
* [Prettier](https://github.com/prettier/prettier)
* [ESLint](https://github.com/eslint/eslint)
* [TSLint](https://github.com/palantir/tslint).
Most (if not all) editors have integrations for these tools so that they will report errors and fix formatting in realtime. See [tooling](./tooling.md) for how to set these integrations up while developing for desktop.

View file

@ -1,11 +1,11 @@
# TypeScript Style Guide
Most of our preferred style when writing typescript is configured in our [`tslint.json`](../../tslint.json) and [`.eslintrc.yml`](../../.eslintrc.yml) files.
Most of our preferred style when writing typescript is configured in our [`.eslintrc.yml`](../../.eslintrc.yml) files.
## Do
- Use camelCase for methods
- Use PascalCase for class names
- Enable [TSLint](https://palantir.github.io/tslint/usage/third-party-tools/) and [ESLint](https://eslint.org/docs/user-guide/integrations) in your editor
- [ESLint](https://eslint.org/docs/user-guide/integrations) in your editor
# Documenting your code

View file

@ -9,8 +9,7 @@ Recommended packages:
* [build-npm-apm](https://atom.io/packages/build-npm-apm) - invoke
all npm scripts straight from the editor by pressing F7 (requires
[build](https://atom.io/packages/build))
* [linter](https://atom.io/packages/linter) and
[linter-tslint](https://atom.io/packages/linter-tslint) - shows linter errors and warning in the editor
* [linter](https://atom.io/packages/linter) - shows linter errors and warning in the editor
You can install them all at once with:

View file

@ -29,9 +29,8 @@
"lint": "yarn prettier && yarn lint:src",
"lint:fix": "yarn prettier --write && yarn lint:src:fix",
"prettier": "prettier --check \"./**/*.{ts,tsx,js,json,jsx,scss,html,yaml,yml}\"",
"lint:src": "yarn tslint && yarn eslint-check && yarn eslint",
"lint:src:fix": "yarn tslint --fix && yarn eslint --fix",
"tslint": "tslint -p .",
"lint:src": "yarn eslint-check && yarn eslint",
"lint:src:fix": "yarn eslint --fix",
"eslint": "eslint --cache --rulesdir ./eslint-rules \"./eslint-rules/**/*.js\" \"./script/**/*.ts{,x}\" \"./app/{src,typings,test}/**/*.{j,t}s{,x}\" \"./changelog.json\"",
"eslint-check": "eslint --print-config .eslintrc.* | eslint-config-prettier-check",
"publish": "ts-node -P script/tsconfig.json script/publish.ts",
@ -60,14 +59,13 @@
"@types/marked": "^4.0.1",
"@types/plist": "^3.0.2",
"@types/react-color": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^3.5.0",
"@typescript-eslint/experimental-utils": "^3.5.0",
"@typescript-eslint/parser": "^3.5.0",
"@typescript-eslint/typescript-estree": "^3.5.0",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/experimental-utils": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"@typescript-eslint/typescript-estree": "^5.10.2",
"airbnb-browser-shims": "^3.0.0",
"ajv": "^6.4.0",
"awesome-node-loader": "^1.1.0",
"awesome-typescript-loader": "^5.2.1",
"azure-storage": "^2.10.4",
"chalk": "^2.2.0",
"clean-webpack-plugin": "^0.1.19",
@ -76,11 +74,10 @@
"css-loader": "^2.1.0",
"eslint": "^7.3.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-jsdoc": "^28.6.1",
"eslint-plugin-jsdoc": "^37.7.0",
"eslint-plugin-json": "^2.1.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react": "7.26.1",
"express": "^4.15.0",
"fake-indexeddb": "^2.0.4",
"file-loader": "^2.0.0",
@ -108,12 +105,9 @@
"style-loader": "^0.21.0",
"to-camel-case": "^1.0.0",
"ts-jest": "^26.4.4",
"ts-loader": "^8",
"ts-node": "^7.0.0",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.14.0",
"tslint-microsoft-contrib": "^6.2.0",
"typescript": "^3.9.5",
"typescript-tslint-plugin": "^0.0.6",
"typescript": "^4.5.5",
"webpack": "^4.8.3",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-dev-middleware": "^3.1.3",

View file

@ -273,60 +273,27 @@ function moveAnalysisFiles() {
}
function copyDependencies() {
const originalPackage: Package = require(path.join(
projectRoot,
'app',
'package.json'
))
const pkg: Package = require(path.join(projectRoot, 'app', 'package.json'))
const oldDependencies = originalPackage.dependencies
const newDependencies: PackageLookup = {}
for (const name of Object.keys(oldDependencies)) {
const spec = oldDependencies[name]
if (externals.indexOf(name) !== -1) {
newDependencies[name] = spec
}
}
const oldDevDependencies = originalPackage.devDependencies
const newDevDependencies: PackageLookup = {}
if (isDevelopmentBuild) {
for (const name of Object.keys(oldDevDependencies)) {
const spec = oldDevDependencies[name]
if (externals.indexOf(name) !== -1) {
newDevDependencies[name] = spec
}
}
}
const filterExternals = (dependencies: Record<string, string>) =>
Object.fromEntries(
Object.entries(dependencies).filter(([k]) => externals.includes(k))
)
// The product name changes depending on whether it's a prod build or dev
// build, so that we can have them running side by side.
const updatedPackage = Object.assign({}, originalPackage, {
productName: getProductName(),
dependencies: newDependencies,
devDependencies: newDevDependencies,
})
if (!isDevelopmentBuild) {
delete updatedPackage.devDependencies
}
fs.writeFileSync(
path.join(outRoot, 'package.json'),
JSON.stringify(updatedPackage)
)
pkg.productName = getProductName()
pkg.dependencies = filterExternals(pkg.dependencies)
pkg.devDependencies =
isDevelopmentBuild && pkg.devDependencies
? filterExternals(pkg.devDependencies)
: {}
fs.writeFileSync(path.join(outRoot, 'package.json'), JSON.stringify(pkg))
fs.removeSync(path.resolve(outRoot, 'node_modules'))
if (
Object.keys(newDependencies).length ||
Object.keys(newDevDependencies).length
) {
console.log(' Installing dependencies via yarn…')
cp.execSync('yarn install', { cwd: outRoot, env: process.env })
}
console.log(' Installing dependencies via yarn…')
cp.execSync('yarn install', { cwd: outRoot, env: process.env })
console.log(' Copying desktop-trampoline…')
const desktopTrampolineDir = path.resolve(outRoot, 'desktop-trampoline')

View file

@ -120,7 +120,7 @@ export async function run(args: ReadonlyArray<string>): Promise<void> {
console.log(`Set!`)
} catch (e) {
console.warn(`Setting the app version failed 😿
(${e.message})
(${e instanceof Error ? e.message : e})
Please manually set it to ${nextVersion} in app/package.json.`)
}
@ -177,7 +177,11 @@ export async function run(args: ReadonlyArray<string>): Promise<void> {
console.log('Added!')
printInstructions(nextVersion, [])
} catch (e) {
console.warn(`Writing the changelog failed 😿\n(${e.message})`)
console.warn(
`Writing the changelog failed 😿\n(${
e instanceof Error ? e.message : e
})`
)
printInstructions(nextVersion, newEntries)
}
} else {

8
script/globals.d.ts vendored
View file

@ -1,9 +1,7 @@
// type annotations for package.json dependencies
type PackageLookup = { [key: string]: string }
type Package = {
dependencies: PackageLookup
devDependencies: PackageLookup
productName?: string
dependencies: Record<string, string>
devDependencies?: Record<string, string>
}
declare namespace NodeJS {

View file

@ -14,15 +14,10 @@
"sourceMap": true,
"jsx": "react",
"strict": true,
"noEmit": true,
"outDir": "./out",
"lib": ["ES2017", "DOM", "DOM.Iterable", "ES2018.Promise", "ES2020.string"]
"lib": ["ES2017", "DOM", "DOM.Iterable", "ES2018.Promise", "ES2020.string"],
"useUnknownInCatchVariables": false
},
"plugins": [
{
"name": "typescript-tslint-plugin"
}
],
"include": ["app/**/*.ts", "app/**/*.tsx", "app/**/*.d.tsx"],
"exclude": [
"node_modules",

View file

@ -1,15 +0,0 @@
{
"extends": ["tslint-config-prettier"],
"rulesDirectory": ["node_modules/tslint-microsoft-contrib/"],
"rules": {
"promise-must-complete": true,
"react-unused-props-and-state": [
true,
{
"props-interface-regex": "Props$",
"state-interface-regex": "State$"
}
],
"react-this-binding-issue": true
}
}

1012
yarn.lock

File diff suppressed because it is too large Load diff