chore: refactor browser IPC into TS and app API into TS (#16921)

* chore: refactor browser IPC into typescript

* chore: refactor app.ts into Typescript

* Refactors app.dock into cpp
* Removes app.launcher which has not existed for 3 years
* Removes 2 deprecated APIs (that have been deprecated for more than one
major)
* Refactors deprecate.ts as well
This commit is contained in:
Samuel Attard 2019-02-14 14:29:20 -08:00 committed by GitHub
parent 4ccd6d5900
commit 5790869a3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 258 additions and 201 deletions

View file

@ -1259,6 +1259,46 @@ bool App::MoveToApplicationsFolder(mate::Arguments* args) {
bool App::IsInApplicationsFolder() {
return ui::cocoa::AtomBundleMover::IsCurrentAppInApplicationsFolder();
}
int DockBounce(const std::string& type) {
int request_id = -1;
if (type == "critical")
request_id = Browser::Get()->DockBounce(Browser::BOUNCE_CRITICAL);
else if (type == "informational")
request_id = Browser::Get()->DockBounce(Browser::BOUNCE_INFORMATIONAL);
return request_id;
}
void DockSetMenu(atom::api::Menu* menu) {
Browser::Get()->DockSetMenu(menu->model());
}
v8::Local<v8::Value> App::GetDockAPI(v8::Isolate* isolate) {
if (dock_.IsEmpty()) {
// Initialize the Dock API, the methods are bound to "dock" which exists
// for the lifetime of "app"
auto browser = base::Unretained(Browser::Get());
mate::Dictionary dock_obj = mate::Dictionary::CreateEmpty(isolate);
dock_obj.SetMethod("bounce", &DockBounce);
dock_obj.SetMethod("cancelBounce",
base::Bind(&Browser::DockCancelBounce, browser));
dock_obj.SetMethod("downloadFinished",
base::Bind(&Browser::DockDownloadFinished, browser));
dock_obj.SetMethod("setBadge",
base::Bind(&Browser::DockSetBadgeText, browser));
dock_obj.SetMethod("getBadge",
base::Bind(&Browser::DockGetBadgeText, browser));
dock_obj.SetMethod("hide", base::Bind(&Browser::DockHide, browser));
dock_obj.SetMethod("show", base::Bind(&Browser::DockShow, browser));
dock_obj.SetMethod("isVisible",
base::Bind(&Browser::DockIsVisible, browser));
dock_obj.SetMethod("setMenu", &DockSetMenu);
dock_obj.SetMethod("setIcon", base::Bind(&Browser::DockSetIcon, browser));
dock_.Reset(isolate, dock_obj.GetHandle());
}
return v8::Local<v8::Value>::New(isolate, dock_);
}
#endif
// static
@ -1357,6 +1397,9 @@ void App::BuildPrototype(v8::Isolate* isolate,
#if defined(MAS_BUILD)
.SetMethod("startAccessingSecurityScopedResource",
&App::StartAccessingSecurityScopedResource)
#endif
#if defined(OS_MACOSX)
.SetProperty("dock", &App::GetDockAPI)
#endif
.SetMethod("enableSandbox", &App::EnableSandbox);
}
@ -1367,21 +1410,6 @@ void App::BuildPrototype(v8::Isolate* isolate,
namespace {
#if defined(OS_MACOSX)
int DockBounce(const std::string& type) {
int request_id = -1;
if (type == "critical")
request_id = Browser::Get()->DockBounce(Browser::BOUNCE_CRITICAL);
else if (type == "informational")
request_id = Browser::Get()->DockBounce(Browser::BOUNCE_INFORMATIONAL);
return request_id;
}
void DockSetMenu(atom::api::Menu* menu) {
Browser::Get()->DockSetMenu(menu->model());
}
#endif
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
@ -1392,23 +1420,6 @@ void Initialize(v8::Local<v8::Object> exports,
->GetFunction(context)
.ToLocalChecked());
dict.Set("app", atom::api::App::Create(isolate));
#if defined(OS_MACOSX)
auto browser = base::Unretained(Browser::Get());
dict.SetMethod("dockBounce", &DockBounce);
dict.SetMethod("dockCancelBounce",
base::Bind(&Browser::DockCancelBounce, browser));
dict.SetMethod("dockDownloadFinished",
base::Bind(&Browser::DockDownloadFinished, browser));
dict.SetMethod("dockSetBadgeText",
base::Bind(&Browser::DockSetBadgeText, browser));
dict.SetMethod("dockGetBadgeText",
base::Bind(&Browser::DockGetBadgeText, browser));
dict.SetMethod("dockHide", base::Bind(&Browser::DockHide, browser));
dict.SetMethod("dockShow", base::Bind(&Browser::DockShow, browser));
dict.SetMethod("dockIsVisible", base::Bind(&Browser::DockIsVisible, browser));
dict.SetMethod("dockSetMenu", &DockSetMenu);
dict.SetMethod("dockSetIcon", base::Bind(&Browser::DockSetIcon, browser));
#endif
}
} // namespace

View file

@ -210,6 +210,8 @@ class App : public AtomBrowserClient::Delegate,
#if defined(OS_MACOSX)
bool MoveToApplicationsFolder(mate::Arguments* args);
bool IsInApplicationsFolder();
v8::Local<v8::Value> GetDockAPI(v8::Isolate* isolate);
v8::Global<v8::Value> dock_;
#endif
#if defined(MAS_BUILD)
base::Callback<void()> StartAccessingSecurityScopedResource(

View file

@ -1307,6 +1307,10 @@ Returns `Boolean` - Whether the dock icon is visible.
Sets the application's [dock menu][dock-menu].
### `app.dock.getMenu()` _macOS_
Returns `Menu | null` - The application's [dock menu][dock-menu].
### `app.dock.setIcon(image)` _macOS_
* `image` ([NativeImage](native-image.md) | String)

View file

@ -1,6 +1,6 @@
filenames = {
js_sources = [
"lib/browser/api/app.js",
"lib/browser/api/app.ts",
"lib/browser/api/auto-updater.js",
"lib/browser/api/auto-updater/auto-updater-native.js",
"lib/browser/api/auto-updater/auto-updater-win.js",
@ -12,7 +12,7 @@ filenames = {
"lib/browser/api/dialog.js",
"lib/browser/api/exports/electron.js",
"lib/browser/api/global-shortcut.js",
"lib/browser/api/ipc-main.js",
"lib/browser/api/ipc-main.ts",
"lib/browser/api/in-app-purchase.js",
"lib/browser/api/menu-item-roles.js",
"lib/browser/api/menu-item.js",
@ -41,13 +41,13 @@ filenames = {
"lib/browser/guest-view-manager.js",
"lib/browser/guest-window-manager.js",
"lib/browser/init.ts",
"lib/browser/ipc-main-internal-utils.js",
"lib/browser/ipc-main-internal.js",
"lib/browser/ipc-main-internal-utils.ts",
"lib/browser/ipc-main-internal.ts",
"lib/browser/navigation-controller.js",
"lib/browser/objects-registry.js",
"lib/browser/rpc-server.js",
"lib/common/api/clipboard.js",
"lib/common/api/deprecate.js",
"lib/common/api/deprecate.ts",
"lib/common/api/deprecations.js",
"lib/common/api/is-promise.js",
"lib/common/api/exports/electron.js",

View file

@ -1,113 +0,0 @@
'use strict'
const bindings = process.atomBinding('app')
const commandLine = process.atomBinding('command_line')
const path = require('path')
const { app, App } = bindings
// Only one app object permitted.
module.exports = app
const electron = require('electron')
const { deprecate, Menu } = electron
const { EventEmitter } = require('events')
let dockMenu = null
// App is an EventEmitter.
Object.setPrototypeOf(App.prototype, EventEmitter.prototype)
EventEmitter.call(app)
Object.assign(app, {
setApplicationMenu (menu) {
return Menu.setApplicationMenu(menu)
},
getApplicationMenu () {
return Menu.getApplicationMenu()
},
commandLine: {
hasSwitch: (...args) => commandLine.hasSwitch(...args.map(String)),
getSwitchValue: (...args) => commandLine.getSwitchValue(...args.map(String)),
appendSwitch: (...args) => commandLine.appendSwitch(...args.map(String)),
appendArgument: (...args) => commandLine.appendArgument(...args.map(String))
},
enableMixedSandbox () {
deprecate.log(`'enableMixedSandbox' is deprecated. Mixed-sandbox mode is now enabled by default. You can safely remove the call to enableMixedSandbox().`)
}
})
app.getFileIcon = deprecate.promisify(app.getFileIcon)
const nativeAppMetrics = app.getAppMetrics
app.getAppMetrics = () => {
const metrics = nativeAppMetrics.call(app)
for (const metric of metrics) {
if ('memory' in metric) {
deprecate.removeProperty(metric, 'memory')
}
}
return metrics
}
app.isPackaged = (() => {
const execFile = path.basename(process.execPath).toLowerCase()
if (process.platform === 'win32') {
return execFile !== 'electron.exe'
}
return execFile !== 'electron'
})()
if (process.platform === 'darwin') {
app.dock = {
bounce (type = 'informational') {
return bindings.dockBounce(type)
},
cancelBounce: bindings.dockCancelBounce,
downloadFinished: bindings.dockDownloadFinished,
setBadge: bindings.dockSetBadgeText,
getBadge: bindings.dockGetBadgeText,
hide: bindings.dockHide,
show: bindings.dockShow,
isVisible: bindings.dockIsVisible,
setMenu (menu) {
dockMenu = menu
bindings.dockSetMenu(menu)
},
getMenu () {
return dockMenu
},
setIcon: bindings.dockSetIcon
}
}
if (process.platform === 'linux') {
app.launcher = {
setBadgeCount: bindings.unityLauncherSetBadgeCount,
getBadgeCount: bindings.unityLauncherGetBadgeCount,
isCounterBadgeAvailable: bindings.unityLauncherAvailable,
isUnityRunning: bindings.unityLauncherAvailable
}
}
app.allowNTLMCredentialsForAllDomains = function (allow) {
deprecate.warn('app.allowNTLMCredentialsForAllDomains', 'session.allowNTLMCredentialsForDomains')
const domains = allow ? '*' : ''
if (!this.isReady()) {
this.commandLine.appendSwitch('auth-server-whitelist', domains)
} else {
electron.session.defaultSession.allowNTLMCredentialsForDomains(domains)
}
}
// Routes the events to webContents.
const events = ['login', 'certificate-error', 'select-client-certificate']
for (const name of events) {
app.on(name, (event, webContents, ...args) => {
webContents.emit(name, event, ...args)
})
}
// Wrappers for native classes.
const { DownloadItem } = process.atomBinding('download_item')
Object.setPrototypeOf(DownloadItem.prototype, EventEmitter.prototype)

68
lib/browser/api/app.ts Normal file
View file

@ -0,0 +1,68 @@
import * as path from 'path'
import * as electron from 'electron'
import { EventEmitter } from 'events'
const bindings = process.atomBinding('app')
const commandLine = process.atomBinding('command_line')
const { app, App } = bindings
// Only one app object permitted.
export default app
const { deprecate, Menu } = electron
let dockMenu: Electron.Menu | null = null
// App is an EventEmitter.
Object.setPrototypeOf(App.prototype, EventEmitter.prototype)
EventEmitter.call(app as any)
Object.assign(app, {
setApplicationMenu (menu: Electron.Menu | null) {
return Menu.setApplicationMenu(menu)
},
getApplicationMenu () {
return Menu.getApplicationMenu()
},
commandLine: {
hasSwitch: (theSwitch: string) => commandLine.hasSwitch(String(theSwitch)),
getSwitchValue: (theSwitch: string) => commandLine.getSwitchValue(String(theSwitch)),
appendSwitch: (theSwitch: string, value?: string) => commandLine.appendSwitch(String(theSwitch), typeof value === 'undefined' ? value : String(value)),
appendArgument: (arg: string) => commandLine.appendArgument(String(arg))
} as Electron.CommandLine,
enableMixedSandbox () {
deprecate.log(`'enableMixedSandbox' is deprecated. Mixed-sandbox mode is now enabled by default. You can safely remove the call to enableMixedSandbox().`)
}
})
app.getFileIcon = deprecate.promisify(app.getFileIcon)
app.isPackaged = (() => {
const execFile = path.basename(process.execPath).toLowerCase()
if (process.platform === 'win32') {
return execFile !== 'electron.exe'
}
return execFile !== 'electron'
})()
if (process.platform === 'darwin') {
const setDockMenu = app.dock.setMenu
app.dock.setMenu = (menu) => {
dockMenu = menu
setDockMenu(menu)
}
app.dock.getMenu = () => dockMenu
}
// Routes the events to webContents.
const events = ['login', 'certificate-error', 'select-client-certificate']
for (const name of events) {
app.on(name as 'login', (event, webContents, ...args: any[]) => {
webContents.emit(name, event, ...args)
})
}
// Wrappers for native classes.
const { DownloadItem } = process.atomBinding('download_item')
Object.setPrototypeOf(DownloadItem.prototype, EventEmitter.prototype)

View file

@ -1,10 +1,8 @@
'use strict'
const { EventEmitter } = require('events')
import { EventEmitter } from 'events'
const emitter = new EventEmitter()
// Do not throw exception when channel name is "error".
emitter.on('error', () => {})
module.exports = emitter
export default emitter

View file

@ -1,9 +1,9 @@
'use strict'
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal'
import * as errorUtils from '@electron/internal/common/error-utils'
const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal')
const errorUtils = require('@electron/internal/common/error-utils')
type IPCHandler = (...args: any[]) => any
const callHandler = async function (handler, event, args, reply) {
const callHandler = async function (handler: IPCHandler, event: Electron.Event, args: any[], reply: (args: any[]) => void) {
try {
const result = await handler(event, ...args)
reply([null, result])
@ -12,7 +12,7 @@ const callHandler = async function (handler, event, args, reply) {
}
}
exports.handle = function (channel, handler) {
export const handle = function <T extends IPCHandler> (channel: string, handler: T) {
ipcMainInternal.on(channel, (event, requestId, ...args) => {
callHandler(handler, event, args, responseArgs => {
event._replyInternal(`${channel}_RESPONSE_${requestId}`, ...responseArgs)
@ -20,7 +20,7 @@ exports.handle = function (channel, handler) {
})
}
exports.handleSync = function (channel, handler) {
export const handleSync = function <T extends IPCHandler> (channel: string, handler: T) {
ipcMainInternal.on(channel, (event, ...args) => {
callHandler(handler, event, args, responseArgs => {
event.returnValue = responseArgs

View file

@ -1,12 +1,8 @@
'use strict'
const { EventEmitter } = require('events')
import { EventEmitter } from 'events'
const emitter = new EventEmitter()
// Do not throw exception when channel name is "error".
emitter.on('error', () => {})
module.exports = {
ipcMainInternal: emitter
}
export const ipcMainInternal = emitter

View file

@ -1,8 +1,6 @@
'use strict'
let deprecationHandler: ElectronInternal.DeprecationHandler | null = null
let deprecationHandler = null
function warnOnce (oldName, newName) {
function warnOnce (oldName: string, newName?: string) {
let warned = false
const msg = newName
? `'${oldName}' is deprecated and will be removed. Please use '${newName}' instead.`
@ -15,7 +13,7 @@ function warnOnce (oldName, newName) {
}
}
const deprecate = {
const deprecate: ElectronInternal.DeprecationUtil = {
setHandler: (handler) => { deprecationHandler = handler },
getHandler: () => deprecationHandler,
warn: (oldName, newName) => {
@ -37,7 +35,7 @@ const deprecate = {
function: (fn, newName) => {
const warn = warnOnce(fn.name, newName)
return function () {
return function (this: any) {
warn()
fn.apply(this, arguments)
}
@ -47,7 +45,7 @@ const deprecate = {
const warn = newName.startsWith('-') /* internal event */
? warnOnce(`${oldName} event`)
: warnOnce(`${oldName} event`, `${newName} event`)
return emitter.on(newName, function (...args) {
return emitter.on(newName, function (this: NodeJS.EventEmitter, ...args) {
if (this.listenerCount(oldName) !== 0) {
warn()
this.emit(oldName, ...args)
@ -77,14 +75,14 @@ const deprecate = {
})
},
promisify: (fn) => {
promisify: <T extends (...args: any[]) => any>(fn: T): T => {
const fnName = fn.name || 'function'
const oldName = `${fnName} with callbacks`
const newName = `${fnName} with Promises`
const warn = warnOnce(oldName, newName)
return function (...params) {
let cb
return function (this: any, ...params: any[]) {
let cb: Function | undefined
if (params.length > 0 && typeof params[params.length - 1] === 'function') {
cb = params.pop()
}
@ -92,26 +90,26 @@ const deprecate = {
if (!cb) return promise
if (process.enablePromiseAPIs) warn()
return promise
.then(res => {
.then((res: any) => {
process.nextTick(() => {
cb.length === 2 ? cb(null, res) : cb(res)
cb!.length === 2 ? cb!(null, res) : cb!(res)
})
}, err => {
}, (err: Error) => {
process.nextTick(() => {
cb.length === 2 ? cb(err) : cb()
cb!.length === 2 ? cb!(err) : cb!()
})
})
}
} as T
},
promisifyMultiArg: (fn) => {
promisifyMultiArg: <T extends (...args: any[]) => any>(fn: T): T => {
const fnName = fn.name || 'function'
const oldName = `${fnName} with callbacks`
const newName = `${fnName} with Promises`
const warn = warnOnce(oldName, newName)
return function (...params) {
let cb
return function (this: any, ...params) {
let cb: Function | undefined
if (params.length > 0 && typeof params[params.length - 1] === 'function') {
cb = params.pop()
}
@ -119,15 +117,15 @@ const deprecate = {
if (!cb) return promise
if (process.enablePromiseAPIs) warn()
return promise
.then(res => {
.then((res: any) => {
process.nextTick(() => {
// eslint-disable-next-line standard/no-callback-literal
cb.length > 2 ? cb(null, ...res) : cb(...res)
cb!.length > 2 ? cb!(null, ...res) : cb!(...res)
})
}, err => {
process.nextTick(() => cb(err))
}, (err: Error) => {
process.nextTick(() => cb!(err))
})
}
} as T
},
renameProperty: (o, oldName, newName) => {
@ -137,7 +135,7 @@ const deprecate = {
// inject it and warn about it
if ((oldName in o) && !(newName in o)) {
warn()
o[newName] = o[oldName]
o[newName] = (o as any)[oldName]
}
// wrap the deprecated property in an accessor to warn
@ -155,4 +153,4 @@ const deprecate = {
}
}
module.exports = deprecate
export default deprecate

View file

@ -24,7 +24,12 @@ exports.defineProperties = function (targetExports) {
for (const module of moduleList) {
descriptors[module.name] = {
enumerable: !module.private,
get: exports.memoizedGetter(() => require(`@electron/internal/common/api/${module.file}`))
get: exports.memoizedGetter(() => {
const value = require(`@electron/internal/common/api/${module.file}.js`)
// Handle Typescript modules with an "export default X" statement
if (value.__esModule) return value.default
return value
})
}
}
return Object.defineProperties(targetExports, descriptors)

View file

@ -18,6 +18,11 @@ for (const {
Object.defineProperty(exports, name, {
enumerable: !isPrivate,
get: common.memoizedGetter(() => require(`@electron/internal/renderer/api/${file}`))
get: common.memoizedGetter(() => {
const value = require(`@electron/internal/renderer/api/${file}.js`)
// Handle Typescript modules with an "export default X" statement
if (value.__esModule) return value.default
return value
})
})
}

View file

@ -2,6 +2,12 @@
const moduleList = require('@electron/internal/sandboxed_renderer/api/module-list')
const handleESModule = (m) => {
// Handle Typescript modules with an "export default X" statement
if (m.__esModule) return m.default
return m
}
for (const {
name,
load,
@ -14,6 +20,6 @@ for (const {
Object.defineProperty(exports, name, {
enumerable: !isPrivate,
get: load
get: () => handleESModule(load())
})
}

View file

@ -1110,20 +1110,69 @@ describe('app module', () => {
})
})
describe('dock APIs', () => {
describe('dock.setMenu()', () => {
it('keeps references to the menu', function () {
if (process.platform !== 'darwin') this.skip()
const dockDescribe = process.platform === 'darwin' ? describe : describe.skip
dockDescribe('dock APIs', () => {
describe('dock.setMenu', () => {
it('can be retrieved via dock.getMenu', () => {
expect(app.dock.getMenu()).to.equal(null)
const menu = new Menu()
app.dock.setMenu(menu)
expect(app.dock.getMenu()).to.equal(menu)
})
it('keeps references to the menu', () => {
app.dock.setMenu(new Menu())
const v8Util = process.atomBinding('v8_util')
v8Util.requestGarbageCollectionForTesting()
})
})
describe('dock.show()', () => {
before(function () {
if (process.platform !== 'darwin') this.skip()
describe('dock.bounce', () => {
it('should return -1 for unknown bounce type', () => {
expect(app.dock.bounce('bad type')).to.equal(-1)
})
it('should return a positive number for informational type', () => {
const appHasFocus = !!BrowserWindow.getFocusedWindow()
if (!appHasFocus) {
expect(app.dock.bounce('informational')).to.be.at.least(0)
}
})
it('should return a positive number for critical type', () => {
const appHasFocus = !!BrowserWindow.getFocusedWindow()
if (!appHasFocus) {
expect(app.dock.bounce('critical')).to.be.at.least(0)
}
})
})
describe('dock.cancelBounce', () => {
it('should not throw', () => {
app.dock.cancelBounce(app.dock.bounce('critical'))
})
})
describe('dock.setBadge', () => {
after(() => {
app.dock.setBadge('')
})
it('should not throw', () => {
app.dock.setBadge('1')
})
it('should be retrievable via getBadge', () => {
app.dock.setBadge('test')
expect(app.dock.getBadge()).to.equal('test')
})
})
describe('dock.show', () => {
it('should not throw', () => {
return app.dock.show().then(() => {
expect(app.dock.isVisible()).to.equal(true)
})
})
it('returns a Promise', () => {
@ -1134,6 +1183,13 @@ describe('app module', () => {
expect(app.dock.show()).to.be.eventually.fulfilled()
})
})
describe('dock.hide', () => {
it('should not throw', () => {
app.dock.hide()
expect(app.dock.isVisible()).to.equal(false)
})
})
})
describe('whenReady', () => {

View file

@ -22,6 +22,8 @@ declare namespace NodeJS {
atomBinding(name: string): any;
atomBinding(name: 'features'): FeaturesBinding;
atomBinding(name: 'v8_util'): V8UtilBinding;
atomBinding(name: 'app'): { app: Electron.App, App: Function };
atomBinding(name: 'command_line'): Electron.CommandLine;
log: NodeJS.WriteStream['write'];
activateUvLoop(): void;
}

View file

@ -26,4 +26,23 @@ declare namespace Electron {
cause: SerializedError,
__ELECTRON_SERIALIZED_ERROR__: true
}
const deprecate: ElectronInternal.DeprecationUtil;
}
declare namespace ElectronInternal {
type DeprecationHandler = (message: string) => void;
interface DeprecationUtil {
setHandler(handler: DeprecationHandler): void;
getHandler(): DeprecationHandler | null;
warn(oldName: string, newName: string): void;
log(message: string): void;
function(fn: Function, newName: string): Function;
event(emitter: NodeJS.EventEmitter, oldName: string, newName: string): void;
removeProperty<T, K extends (keyof T & string)>(object: T, propertyName: K): T;
renameProperty<T, K extends (keyof T & string)>(object: T, oldName: string, newName: K): T;
promisify<T extends (...args: any[]) => any>(fn: T): T;
promisifyMultiArg<T extends (...args: any[]) => any>(fn: T): T;
}
}