mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2024-12-22 10:08:55 +00:00
Make app strict TypeScript + linting (and add a shared project) (#1231)
* Convert app to strict typing + shared library * Fix new code post-merge * Remove extraneous lint ignores * Apply suggestions from code review Co-authored-by: Ronan Jouchet <ronan@jouchet.fr> * Fix prettier complaint * Dedupe eslint files * Fix some refs after merge * Fix clean:full command Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
This commit is contained in:
parent
f8f48d2f09
commit
b74c0bf959
2
.github/manual-test
vendored
2
.github/manual-test
vendored
@ -13,7 +13,7 @@ function launch_app() {
|
|||||||
if [ "$(uname -s)" = "Darwin" ]; then
|
if [ "$(uname -s)" = "Darwin" ]; then
|
||||||
open -a "$1/$2-darwin-x64/$2.app"
|
open -a "$1/$2-darwin-x64/$2.app"
|
||||||
elif [ "$(uname -o)" = "Msys" ]; then
|
elif [ "$(uname -o)" = "Msys" ]; then
|
||||||
"$1/$2-win32-x64/$2.exe" --verbose
|
"$1/$2-win32-x64/$2.exe"
|
||||||
else
|
else
|
||||||
"$1/$2-linux-x64/$2"
|
"$1/$2-linux-x64/$2"
|
||||||
fi
|
fi
|
||||||
|
@ -1,29 +1,21 @@
|
|||||||
|
const base = require('../base-eslintrc');
|
||||||
|
|
||||||
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
|
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parser: '@typescript-eslint/parser',
|
parser: base.parser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: ['./tsconfig.json'],
|
project: ['./tsconfig.json'],
|
||||||
},
|
},
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: base.plugins,
|
||||||
extends: [
|
extends: base.extends,
|
||||||
'eslint:recommended',
|
rules: base.rules,
|
||||||
'prettier',
|
// https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
ignorePatterns: [
|
||||||
'plugin:@typescript-eslint/recommended',
|
'node_modules/**',
|
||||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
'lib/**',
|
||||||
|
'dist/**',
|
||||||
|
'built-tests/**',
|
||||||
|
'coverage/**',
|
||||||
],
|
],
|
||||||
rules: {
|
|
||||||
'no-console': 'error',
|
|
||||||
'prettier/prettier': 'error',
|
|
||||||
// TODO remove when done killing `any`s and making tsc strict
|
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-call': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-return': 'off',
|
|
||||||
'@typescript-eslint/restrict-template-expressions': 'off'
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
],
|
],
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-context-menu": "^2.5.0",
|
"electron-context-menu": "^3.1.0",
|
||||||
"electron-dl": "^3.2.0",
|
"electron-dl": "^3.2.1",
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"loglevel": "^1.7.1",
|
"loglevel": "^1.7.1",
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow, ContextMenuParams } from 'electron';
|
||||||
|
import contextMenu from 'electron-context-menu';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import { nativeTabsSupported, openExternal } from '../helpers/helpers';
|
import { nativeTabsSupported, openExternal } from '../helpers/helpers';
|
||||||
import { setupNativefierWindow } from '../helpers/windowEvents';
|
import { setupNativefierWindow } from '../helpers/windowEvents';
|
||||||
import { createNewTab, createNewWindow } from '../helpers/windowHelpers';
|
import { createNewTab, createNewWindow } from '../helpers/windowHelpers';
|
||||||
|
import {
|
||||||
|
OutputOptions,
|
||||||
|
outputOptionsToWindowOptions,
|
||||||
|
} from '../../../shared/src/options/model';
|
||||||
|
|
||||||
export function initContextMenu(options, window?: BrowserWindow): void {
|
export function initContextMenu(
|
||||||
// Require this at runtime, otherwise its child dependency 'electron-is-dev'
|
options: OutputOptions,
|
||||||
// throws an error during unit testing.
|
window?: BrowserWindow,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
): void {
|
||||||
const contextMenu = require('electron-context-menu');
|
|
||||||
|
|
||||||
log.debug('initContextMenu', { options, window });
|
log.debug('initContextMenu', { options, window });
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
contextMenu({
|
contextMenu({
|
||||||
prepend: (actions, params) => {
|
prepend: (actions: contextMenu.Actions, params: ContextMenuParams) => {
|
||||||
log.debug('contextMenu.prepend', { actions, params });
|
log.debug('contextMenu.prepend', { actions, params });
|
||||||
const items = [];
|
const items = [];
|
||||||
if (params.linkURL) {
|
if (params.linkURL) {
|
||||||
@ -29,7 +33,7 @@ export function initContextMenu(options, window?: BrowserWindow): void {
|
|||||||
label: 'Open Link in New Window',
|
label: 'Open Link in New Window',
|
||||||
click: () =>
|
click: () =>
|
||||||
createNewWindow(
|
createNewWindow(
|
||||||
options,
|
outputOptionsToWindowOptions(options),
|
||||||
setupNativefierWindow,
|
setupNativefierWindow,
|
||||||
params.linkURL,
|
params.linkURL,
|
||||||
window,
|
window,
|
||||||
@ -40,7 +44,7 @@ export function initContextMenu(options, window?: BrowserWindow): void {
|
|||||||
label: 'Open Link in New Tab',
|
label: 'Open Link in New Tab',
|
||||||
click: () =>
|
click: () =>
|
||||||
createNewTab(
|
createNewTab(
|
||||||
options,
|
outputOptionsToWindowOptions(options),
|
||||||
setupNativefierWindow,
|
setupNativefierWindow,
|
||||||
params.linkURL,
|
params.linkURL,
|
||||||
true,
|
true,
|
||||||
|
@ -5,7 +5,7 @@ import * as log from 'loglevel';
|
|||||||
import { BrowserWindow, ipcMain } from 'electron';
|
import { BrowserWindow, ipcMain } from 'electron';
|
||||||
|
|
||||||
export async function createLoginWindow(
|
export async function createLoginWindow(
|
||||||
loginCallback,
|
loginCallback: (username?: string, password?: string) => void,
|
||||||
parent?: BrowserWindow,
|
parent?: BrowserWindow,
|
||||||
): Promise<BrowserWindow> {
|
): Promise<BrowserWindow> {
|
||||||
log.debug('createLoginWindow', { loginCallback, parent });
|
log.debug('createLoginWindow', { loginCallback, parent });
|
||||||
@ -25,7 +25,7 @@ export async function createLoginWindow(
|
|||||||
`file://${path.join(__dirname, 'static/login.html')}`,
|
`file://${path.join(__dirname, 'static/login.html')}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.once('login-message', (event, usernameAndPassword) => {
|
ipcMain.once('login-message', (event, usernameAndPassword: string[]) => {
|
||||||
log.debug('login-message', { event, username: usernameAndPassword[0] });
|
log.debug('login-message', { event, username: usernameAndPassword[0] });
|
||||||
loginCallback(usernameAndPassword[0], usernameAndPassword[1]);
|
loginCallback(usernameAndPassword[0], usernameAndPassword[1]);
|
||||||
loginWindow.close();
|
loginWindow.close();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { ipcMain, BrowserWindow, IpcMainEvent } from 'electron';
|
import { ipcMain, BrowserWindow, Event } from 'electron';
|
||||||
import windowStateKeeper from 'electron-window-state';
|
import windowStateKeeper from 'electron-window-state';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
|
||||||
@ -19,6 +19,10 @@ import {
|
|||||||
} from '../helpers/windowHelpers';
|
} from '../helpers/windowHelpers';
|
||||||
import { initContextMenu } from './contextMenu';
|
import { initContextMenu } from './contextMenu';
|
||||||
import { createMenu } from './menu';
|
import { createMenu } from './menu';
|
||||||
|
import {
|
||||||
|
OutputOptions,
|
||||||
|
outputOptionsToWindowOptions,
|
||||||
|
} from '../../../shared/src/options/model';
|
||||||
|
|
||||||
export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
||||||
|
|
||||||
@ -32,7 +36,7 @@ type SessionInteractionRequest = {
|
|||||||
|
|
||||||
type SessionInteractionResult = {
|
type SessionInteractionResult = {
|
||||||
id?: string;
|
id?: string;
|
||||||
value?: unknown;
|
value?: unknown | Promise<unknown>;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,7 +45,7 @@ type SessionInteractionResult = {
|
|||||||
* @param {function} setDockBadge
|
* @param {function} setDockBadge
|
||||||
*/
|
*/
|
||||||
export async function createMainWindow(
|
export async function createMainWindow(
|
||||||
nativefierOptions,
|
nativefierOptions: OutputOptions,
|
||||||
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
||||||
): Promise<BrowserWindow> {
|
): Promise<BrowserWindow> {
|
||||||
const options = { ...nativefierOptions };
|
const options = { ...nativefierOptions };
|
||||||
@ -66,10 +70,10 @@ export async function createMainWindow(
|
|||||||
fullscreen: options.fullScreen,
|
fullscreen: options.fullScreen,
|
||||||
// Whether the window should always stay on top of other windows. Default is false.
|
// Whether the window should always stay on top of other windows. Default is false.
|
||||||
alwaysOnTop: options.alwaysOnTop,
|
alwaysOnTop: options.alwaysOnTop,
|
||||||
titleBarStyle: options.titleBarStyle,
|
titleBarStyle: options.titleBarStyle ?? 'default',
|
||||||
show: options.tray !== 'start-in-tray',
|
show: options.tray !== 'start-in-tray',
|
||||||
backgroundColor: options.backgroundColor,
|
backgroundColor: options.backgroundColor,
|
||||||
...getDefaultWindowOptions(options),
|
...getDefaultWindowOptions(outputOptionsToWindowOptions(options)),
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindowState.manage(mainWindow);
|
mainWindowState.manage(mainWindow);
|
||||||
@ -86,7 +90,7 @@ export async function createMainWindow(
|
|||||||
}
|
}
|
||||||
createMenu(options, mainWindow);
|
createMenu(options, mainWindow);
|
||||||
createContextMenu(options, mainWindow);
|
createContextMenu(options, mainWindow);
|
||||||
setupNativefierWindow(options, mainWindow);
|
setupNativefierWindow(outputOptionsToWindowOptions(options), mainWindow);
|
||||||
|
|
||||||
// .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...)
|
// .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...)
|
||||||
// We can't quite cut over to that yet for a few reasons:
|
// We can't quite cut over to that yet for a few reasons:
|
||||||
@ -104,7 +108,7 @@ export async function createMainWindow(
|
|||||||
'new-window',
|
'new-window',
|
||||||
(event, url, frameName, disposition) => {
|
(event, url, frameName, disposition) => {
|
||||||
onNewWindow(
|
onNewWindow(
|
||||||
options,
|
outputOptionsToWindowOptions(options),
|
||||||
setupNativefierWindow,
|
setupNativefierWindow,
|
||||||
event,
|
event,
|
||||||
url,
|
url,
|
||||||
@ -131,20 +135,25 @@ export async function createMainWindow(
|
|||||||
await clearCache(mainWindow);
|
await clearCache(mainWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
await mainWindow.loadURL(options.targetUrl);
|
if (options.targetUrl) {
|
||||||
|
await mainWindow.loadURL(options.targetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
setupCloseEvent(options, mainWindow);
|
setupCloseEvent(options, mainWindow);
|
||||||
|
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createContextMenu(options, window: BrowserWindow): void {
|
function createContextMenu(
|
||||||
|
options: OutputOptions,
|
||||||
|
window: BrowserWindow,
|
||||||
|
): void {
|
||||||
if (!options.disableContextMenu) {
|
if (!options.disableContextMenu) {
|
||||||
initContextMenu(options, window);
|
initContextMenu(options, window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveAppArgs(newAppArgs: any): void {
|
export function saveAppArgs(newAppArgs: OutputOptions): void {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs, null, 2));
|
fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs, null, 2));
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@ -154,19 +163,29 @@ export function saveAppArgs(newAppArgs: any): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCloseEvent(options, window: BrowserWindow): void {
|
function setupCloseEvent(options: OutputOptions, window: BrowserWindow): void {
|
||||||
window.on('close', (event: IpcMainEvent) => {
|
window.on('close', (event: Event) => {
|
||||||
log.debug('mainWindow.close', event);
|
log.debug('mainWindow.close', event);
|
||||||
if (window.isFullScreen()) {
|
if (window.isFullScreen()) {
|
||||||
if (nativeTabsSupported()) {
|
if (nativeTabsSupported()) {
|
||||||
window.moveTabToNewWindow();
|
window.moveTabToNewWindow();
|
||||||
}
|
}
|
||||||
window.setFullScreen(false);
|
window.setFullScreen(false);
|
||||||
window.once('leave-full-screen', (event: IpcMainEvent) =>
|
window.once('leave-full-screen', (event: Event) =>
|
||||||
hideWindow(window, event, options.fastQuit, options.tray),
|
hideWindow(
|
||||||
|
window,
|
||||||
|
event,
|
||||||
|
options.fastQuit ?? false,
|
||||||
|
options.tray ?? 'false',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
hideWindow(window, event, options.fastQuit, options.tray);
|
hideWindow(
|
||||||
|
window,
|
||||||
|
event,
|
||||||
|
options.fastQuit ?? false,
|
||||||
|
options.tray ?? 'false',
|
||||||
|
);
|
||||||
|
|
||||||
if (options.clearCache) {
|
if (options.clearCache) {
|
||||||
clearCache(window).catch((err) => log.error('clearCache ERROR', err));
|
clearCache(window).catch((err) => log.error('clearCache ERROR', err));
|
||||||
@ -175,7 +194,7 @@ function setupCloseEvent(options, window: BrowserWindow): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupCounter(
|
function setupCounter(
|
||||||
options,
|
options: OutputOptions,
|
||||||
window: BrowserWindow,
|
window: BrowserWindow,
|
||||||
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
||||||
): void {
|
): void {
|
||||||
@ -191,7 +210,7 @@ function setupCounter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupNotificationBadge(
|
function setupNotificationBadge(
|
||||||
options,
|
options: OutputOptions,
|
||||||
window: BrowserWindow,
|
window: BrowserWindow,
|
||||||
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
||||||
): void {
|
): void {
|
||||||
@ -208,7 +227,10 @@ function setupNotificationBadge(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSessionInteraction(options, window: BrowserWindow): void {
|
function setupSessionInteraction(
|
||||||
|
options: OutputOptions,
|
||||||
|
window: BrowserWindow,
|
||||||
|
): void {
|
||||||
// See API.md / "Accessing The Electron Session"
|
// See API.md / "Accessing The Electron Session"
|
||||||
ipcMain.on(
|
ipcMain.on(
|
||||||
'session-interaction',
|
'session-interaction',
|
||||||
@ -230,14 +252,13 @@ function setupSessionInteraction(options, window: BrowserWindow): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call func with funcArgs
|
// Call func with funcArgs
|
||||||
|
// @ts-expect-error accessing a func by string name
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
result.value = window.webContents.session[request.func](
|
result.value = window.webContents.session[request.func](
|
||||||
...request.funcArgs,
|
...request.funcArgs,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (result.value !== undefined && result.value instanceof Promise) {
|
||||||
result.value !== undefined &&
|
|
||||||
typeof result.value['then'] === 'function'
|
|
||||||
) {
|
|
||||||
// This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply
|
// This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply
|
||||||
(result.value as Promise<unknown>)
|
(result.value as Promise<unknown>)
|
||||||
.then((trueResultValue) => {
|
.then((trueResultValue) => {
|
||||||
@ -253,11 +274,13 @@ function setupSessionInteraction(options, window: BrowserWindow): void {
|
|||||||
} else if (request.property !== undefined) {
|
} else if (request.property !== undefined) {
|
||||||
if (request.propertyValue !== undefined) {
|
if (request.propertyValue !== undefined) {
|
||||||
// Set the property
|
// Set the property
|
||||||
|
// @ts-expect-error setting a property by string name
|
||||||
window.webContents.session[request.property] =
|
window.webContents.session[request.property] =
|
||||||
request.propertyValue;
|
request.propertyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the property value
|
// Get the property value
|
||||||
|
// @ts-expect-error accessing a property by string name
|
||||||
result.value = window.webContents.session[request.property];
|
result.value = window.webContents.session[request.property];
|
||||||
} else {
|
} else {
|
||||||
// Why even send the event if you're going to do this? You're just wasting time! ;)
|
// Why even send the event if you're going to do this? You're just wasting time! ;)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
||||||
|
|
||||||
jest.mock('../helpers/helpers');
|
jest.mock('../helpers/helpers');
|
||||||
import { isOSX } from '../helpers/helpers';
|
import { isOSX } from '../helpers/helpers';
|
||||||
@ -16,9 +16,15 @@ describe('generateMenu', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
window = new BrowserWindow();
|
window = new BrowserWindow();
|
||||||
mockIsOSX.mockReset();
|
mockIsOSX.mockReset();
|
||||||
mockIsFullScreen = jest.spyOn(window, 'isFullScreen');
|
mockIsFullScreen = jest
|
||||||
mockIsFullScreenable = jest.spyOn(window, 'isFullScreenable');
|
.spyOn(window, 'isFullScreen')
|
||||||
mockIsSimpleFullScreen = jest.spyOn(window, 'isSimpleFullScreen');
|
.mockReturnValue(false);
|
||||||
|
mockIsFullScreenable = jest
|
||||||
|
.spyOn(window, 'isFullScreenable')
|
||||||
|
.mockReturnValue(true);
|
||||||
|
mockIsSimpleFullScreen = jest
|
||||||
|
.spyOn(window, 'isSimpleFullScreen')
|
||||||
|
.mockReturnValue(false);
|
||||||
mockSetFullScreen = jest.spyOn(window, 'setFullScreen');
|
mockSetFullScreen = jest.spyOn(window, 'setFullScreen');
|
||||||
mockSetSimpleFullScreen = jest.spyOn(window, 'setSimpleFullScreen');
|
mockSetSimpleFullScreen = jest.spyOn(window, 'setSimpleFullScreen');
|
||||||
});
|
});
|
||||||
@ -46,9 +52,9 @@ describe('generateMenu', () => {
|
|||||||
|
|
||||||
const editMenu = menu.filter((item) => item.label === '&View');
|
const editMenu = menu.filter((item) => item.label === '&View');
|
||||||
|
|
||||||
const fullscreen = (editMenu[0].submenu as any[]).filter(
|
const fullscreen = (
|
||||||
(item) => item.label === 'Toggle Full Screen',
|
editMenu[0].submenu as MenuItemConstructorOptions[]
|
||||||
);
|
).filter((item) => item.label === 'Toggle Full Screen');
|
||||||
|
|
||||||
expect(fullscreen).toHaveLength(1);
|
expect(fullscreen).toHaveLength(1);
|
||||||
expect(fullscreen[0].enabled).toBe(false);
|
expect(fullscreen[0].enabled).toBe(false);
|
||||||
@ -73,9 +79,9 @@ describe('generateMenu', () => {
|
|||||||
|
|
||||||
const editMenu = menu.filter((item) => item.label === '&View');
|
const editMenu = menu.filter((item) => item.label === '&View');
|
||||||
|
|
||||||
const fullscreen = (editMenu[0].submenu as any[]).filter(
|
const fullscreen = (
|
||||||
(item) => item.label === 'Toggle Full Screen',
|
editMenu[0].submenu as MenuItemConstructorOptions[]
|
||||||
);
|
).filter((item) => item.label === 'Toggle Full Screen');
|
||||||
|
|
||||||
expect(fullscreen).toHaveLength(1);
|
expect(fullscreen).toHaveLength(1);
|
||||||
expect(fullscreen[0].enabled).toBe(true);
|
expect(fullscreen[0].enabled).toBe(true);
|
||||||
@ -103,9 +109,9 @@ describe('generateMenu', () => {
|
|||||||
|
|
||||||
const editMenu = menu.filter((item) => item.label === '&View');
|
const editMenu = menu.filter((item) => item.label === '&View');
|
||||||
|
|
||||||
const fullscreen = (editMenu[0].submenu as any[]).filter(
|
const fullscreen = (
|
||||||
(item) => item.label === 'Toggle Full Screen',
|
editMenu[0].submenu as MenuItemConstructorOptions[]
|
||||||
);
|
).filter((item) => item.label === 'Toggle Full Screen');
|
||||||
|
|
||||||
expect(fullscreen).toHaveLength(1);
|
expect(fullscreen).toHaveLength(1);
|
||||||
expect(fullscreen[0].enabled).toBe(true);
|
expect(fullscreen[0].enabled).toBe(true);
|
||||||
@ -114,6 +120,7 @@ describe('generateMenu', () => {
|
|||||||
expect(mockIsOSX).toHaveBeenCalled();
|
expect(mockIsOSX).toHaveBeenCalled();
|
||||||
expect(mockIsFullScreenable).toHaveBeenCalled();
|
expect(mockIsFullScreenable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// @ts-expect-error click is here TypeScript...
|
||||||
fullscreen[0].click(null, window);
|
fullscreen[0].click(null, window);
|
||||||
|
|
||||||
expect(mockSetFullScreen).toHaveBeenCalledWith(!isFullScreen);
|
expect(mockSetFullScreen).toHaveBeenCalledWith(!isFullScreen);
|
||||||
@ -139,9 +146,9 @@ describe('generateMenu', () => {
|
|||||||
|
|
||||||
const editMenu = menu.filter((item) => item.label === '&View');
|
const editMenu = menu.filter((item) => item.label === '&View');
|
||||||
|
|
||||||
const fullscreen = (editMenu[0].submenu as any[]).filter(
|
const fullscreen = (
|
||||||
(item) => item.label === 'Toggle Full Screen',
|
editMenu[0].submenu as MenuItemConstructorOptions[]
|
||||||
);
|
).filter((item) => item.label === 'Toggle Full Screen');
|
||||||
|
|
||||||
expect(fullscreen).toHaveLength(1);
|
expect(fullscreen).toHaveLength(1);
|
||||||
expect(fullscreen[0].enabled).toBe(true);
|
expect(fullscreen[0].enabled).toBe(true);
|
||||||
@ -150,6 +157,7 @@ describe('generateMenu', () => {
|
|||||||
expect(mockIsOSX).toHaveBeenCalled();
|
expect(mockIsOSX).toHaveBeenCalled();
|
||||||
expect(mockIsFullScreenable).toHaveBeenCalled();
|
expect(mockIsFullScreenable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// @ts-expect-error click is here TypeScript...
|
||||||
fullscreen[0].click(null, window);
|
fullscreen[0].click(null, window);
|
||||||
|
|
||||||
expect(mockSetSimpleFullScreen).toHaveBeenCalledWith(!isFullScreen);
|
expect(mockSetSimpleFullScreen).toHaveBeenCalledWith(!isFullScreen);
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
zoomOut,
|
zoomOut,
|
||||||
zoomReset,
|
zoomReset,
|
||||||
} from '../helpers/windowHelpers';
|
} from '../helpers/windowHelpers';
|
||||||
|
import { OutputOptions } from '../../../shared/src/options/model';
|
||||||
|
|
||||||
type BookmarksLink = {
|
type BookmarksLink = {
|
||||||
type: 'link';
|
type: 'link';
|
||||||
@ -40,7 +41,10 @@ type BookmarksMenuConfig = {
|
|||||||
bookmarks: BookmarkConfig[];
|
bookmarks: BookmarkConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createMenu(options, mainWindow: BrowserWindow): void {
|
export function createMenu(
|
||||||
|
options: OutputOptions,
|
||||||
|
mainWindow: BrowserWindow,
|
||||||
|
): void {
|
||||||
log.debug('createMenu', { options, mainWindow });
|
log.debug('createMenu', { options, mainWindow });
|
||||||
const menuTemplate = generateMenu(options, mainWindow);
|
const menuTemplate = generateMenu(options, mainWindow);
|
||||||
|
|
||||||
@ -51,7 +55,11 @@ export function createMenu(options, mainWindow: BrowserWindow): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateMenu(
|
export function generateMenu(
|
||||||
options,
|
options: {
|
||||||
|
disableDevTools: boolean;
|
||||||
|
nativefierVersion: string;
|
||||||
|
zoom?: number;
|
||||||
|
},
|
||||||
mainWindow: BrowserWindow,
|
mainWindow: BrowserWindow,
|
||||||
): MenuItemConstructorOptions[] {
|
): MenuItemConstructorOptions[] {
|
||||||
const { nativefierVersion, zoom, disableDevTools } = options;
|
const { nativefierVersion, zoom, disableDevTools } = options;
|
||||||
@ -108,7 +116,10 @@ export function generateMenu(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Clear App Data',
|
label: 'Clear App Data',
|
||||||
click: (item: MenuItem, focusedWindow: BrowserWindow): void => {
|
click: (
|
||||||
|
item: MenuItem,
|
||||||
|
focusedWindow: BrowserWindow | undefined,
|
||||||
|
): void => {
|
||||||
log.debug('Clear App Data.click', {
|
log.debug('Clear App Data.click', {
|
||||||
item,
|
item,
|
||||||
focusedWindow,
|
focusedWindow,
|
||||||
@ -164,7 +175,10 @@ export function generateMenu(
|
|||||||
accelerator: isOSX() ? 'Ctrl+Cmd+F' : 'F11',
|
accelerator: isOSX() ? 'Ctrl+Cmd+F' : 'F11',
|
||||||
enabled: mainWindow.isFullScreenable() || isOSX(),
|
enabled: mainWindow.isFullScreenable() || isOSX(),
|
||||||
visible: mainWindow.isFullScreenable() || isOSX(),
|
visible: mainWindow.isFullScreenable() || isOSX(),
|
||||||
click: (item: MenuItem, focusedWindow: BrowserWindow): void => {
|
click: (
|
||||||
|
item: MenuItem,
|
||||||
|
focusedWindow: BrowserWindow | undefined,
|
||||||
|
): void => {
|
||||||
log.debug('Toggle Full Screen.click()', {
|
log.debug('Toggle Full Screen.click()', {
|
||||||
item,
|
item,
|
||||||
focusedWindow,
|
focusedWindow,
|
||||||
@ -230,7 +244,7 @@ export function generateMenu(
|
|||||||
{
|
{
|
||||||
label: 'Toggle Developer Tools',
|
label: 'Toggle Developer Tools',
|
||||||
accelerator: isOSX() ? 'Alt+Cmd+I' : 'Ctrl+Shift+I',
|
accelerator: isOSX() ? 'Alt+Cmd+I' : 'Ctrl+Shift+I',
|
||||||
click: (item: MenuItem, focusedWindow: BrowserWindow) => {
|
click: (item: MenuItem, focusedWindow: BrowserWindow | undefined) => {
|
||||||
log.debug('Toggle Developer Tools.click()', { item, focusedWindow });
|
log.debug('Toggle Developer Tools.click()', { item, focusedWindow });
|
||||||
if (!focusedWindow) {
|
if (!focusedWindow) {
|
||||||
focusedWindow = mainWindow;
|
focusedWindow = mainWindow;
|
||||||
@ -349,12 +363,11 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bookmarksMenuConfig: BookmarksMenuConfig = JSON.parse(
|
const bookmarksMenuConfig = JSON.parse(
|
||||||
fs.readFileSync(bookmarkConfigPath, 'utf-8'),
|
fs.readFileSync(bookmarkConfigPath, 'utf-8'),
|
||||||
);
|
) as BookmarksMenuConfig;
|
||||||
const bookmarksMenu: MenuItemConstructorOptions = {
|
const submenu: MenuItemConstructorOptions[] =
|
||||||
label: bookmarksMenuConfig.menuLabel,
|
bookmarksMenuConfig.bookmarks.map((bookmark) => {
|
||||||
submenu: bookmarksMenuConfig.bookmarks.map((bookmark) => {
|
|
||||||
switch (bookmark.type) {
|
switch (bookmark.type) {
|
||||||
case 'link':
|
case 'link':
|
||||||
if (!('title' in bookmark && 'url' in bookmark)) {
|
if (!('title' in bookmark && 'url' in bookmark)) {
|
||||||
@ -370,11 +383,12 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
|
|||||||
return {
|
return {
|
||||||
label: bookmark.title,
|
label: bookmark.title,
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
goToURL(bookmark.url).catch((err: unknown): void =>
|
goToURL(bookmark.url)?.catch((err: unknown): void =>
|
||||||
log.error(`${bookmark.title}.click ERROR`, err),
|
log.error(`${bookmark.title}.click ERROR`, err),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
accelerator: 'shortcut' in bookmark ? bookmark.shortcut : null,
|
accelerator:
|
||||||
|
'shortcut' in bookmark ? bookmark.shortcut : undefined,
|
||||||
};
|
};
|
||||||
case 'separator':
|
case 'separator':
|
||||||
return {
|
return {
|
||||||
@ -385,7 +399,10 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
|
|||||||
'A bookmarks menu entry has an invalid type; type must be one of "link", "separator".',
|
'A bookmarks menu entry has an invalid type; type must be one of "link", "separator".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
});
|
||||||
|
const bookmarksMenu: MenuItemConstructorOptions = {
|
||||||
|
label: bookmarksMenuConfig.menuLabel,
|
||||||
|
submenu,
|
||||||
};
|
};
|
||||||
// Insert custom bookmarks menu between menus "View" and "Window"
|
// Insert custom bookmarks menu between menus "View" and "Window"
|
||||||
menuTemplate.splice(menuTemplate.length - 2, 0, bookmarksMenu);
|
menuTemplate.splice(menuTemplate.length - 2, 0, bookmarksMenu);
|
||||||
|
@ -2,15 +2,19 @@ import { app, Tray, Menu, ipcMain, nativeImage, BrowserWindow } from 'electron';
|
|||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
|
||||||
import { getAppIcon, getCounterValue, isOSX } from '../helpers/helpers';
|
import { getAppIcon, getCounterValue, isOSX } from '../helpers/helpers';
|
||||||
|
import { OutputOptions } from '../../../shared/src/options/model';
|
||||||
|
|
||||||
export function createTrayIcon(
|
export function createTrayIcon(
|
||||||
nativefierOptions,
|
nativefierOptions: OutputOptions,
|
||||||
mainWindow: BrowserWindow,
|
mainWindow: BrowserWindow,
|
||||||
): Tray {
|
): Tray | undefined {
|
||||||
const options = { ...nativefierOptions };
|
const options = { ...nativefierOptions };
|
||||||
|
|
||||||
if (options.tray && options.tray !== 'false') {
|
if (options.tray && options.tray !== 'false') {
|
||||||
const iconPath = getAppIcon();
|
const iconPath = getAppIcon();
|
||||||
|
if (!iconPath) {
|
||||||
|
throw new Error('Icon path not found found to use with tray option.');
|
||||||
|
}
|
||||||
const nimage = nativeImage.createFromPath(iconPath);
|
const nimage = nativeImage.createFromPath(iconPath);
|
||||||
const appIcon = new Tray(nativeImage.createEmpty());
|
const appIcon = new Tray(nativeImage.createEmpty());
|
||||||
|
|
||||||
@ -39,7 +43,7 @@ export function createTrayIcon(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Quit',
|
label: 'Quit',
|
||||||
click: app.exit.bind(this),
|
click: (): void => app.exit(0),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -50,9 +54,11 @@ export function createTrayIcon(
|
|||||||
log.debug('mainWindow.page-title-updated', { event, title });
|
log.debug('mainWindow.page-title-updated', { event, title });
|
||||||
const counterValue = getCounterValue(title);
|
const counterValue = getCounterValue(title);
|
||||||
if (counterValue) {
|
if (counterValue) {
|
||||||
appIcon.setToolTip(`(${counterValue}) ${options.name}`);
|
appIcon.setToolTip(
|
||||||
|
`(${counterValue}) ${options.name ?? 'Nativefier'}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
appIcon.setToolTip(options.name);
|
appIcon.setToolTip(options.name ?? '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -61,20 +67,22 @@ export function createTrayIcon(
|
|||||||
if (mainWindow.isFocused()) {
|
if (mainWindow.isFocused()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
appIcon.setToolTip(`• ${options.name}`);
|
if (options.name) {
|
||||||
|
appIcon.setToolTip(`• ${options.name}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on('focus', () => {
|
mainWindow.on('focus', () => {
|
||||||
log.debug('mainWindow.focus');
|
log.debug('mainWindow.focus');
|
||||||
appIcon.setToolTip(options.name);
|
appIcon.setToolTip(options.name ?? '');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
appIcon.setToolTip(options.name);
|
appIcon.setToolTip(options.name ?? '');
|
||||||
appIcon.setContextMenu(contextMenu);
|
appIcon.setContextMenu(contextMenu);
|
||||||
|
|
||||||
return appIcon;
|
return appIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ function domainify(url: string): string {
|
|||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAppIcon(): string {
|
export function getAppIcon(): string | undefined {
|
||||||
// Prefer ICO under Windows, see
|
// Prefer ICO under Windows, see
|
||||||
// https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions
|
// https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions
|
||||||
// https://www.electronjs.org/docs/api/native-image#supported-formats
|
// https://www.electronjs.org/docs/api/native-image#supported-formats
|
||||||
@ -55,7 +55,7 @@ export function getAppIcon(): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCounterValue(title: string): string {
|
export function getCounterValue(title: string): string | undefined {
|
||||||
const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/;
|
const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/;
|
||||||
const match = itemCountRegex.exec(title);
|
const match = itemCountRegex.exec(title);
|
||||||
return match ? match[1] : undefined;
|
return match ? match[1] : undefined;
|
||||||
@ -74,7 +74,7 @@ export function getCSSToInject(): string {
|
|||||||
for (const cssFile of cssFiles) {
|
for (const cssFile of cssFiles) {
|
||||||
log.debug('Injecting CSS file', cssFile);
|
log.debug('Injecting CSS file', cssFile);
|
||||||
const cssFileData = fs.readFileSync(cssFile);
|
const cssFileData = fs.readFileSync(cssFile);
|
||||||
cssToInject += `/* ${cssFile} */\n\n ${cssFileData}\n\n`;
|
cssToInject += `/* ${cssFile} */\n\n ${cssFileData.toString()}\n\n`;
|
||||||
}
|
}
|
||||||
return cssToInject;
|
return cssToInject;
|
||||||
}
|
}
|
||||||
@ -114,7 +114,7 @@ function isInternalLoginPage(url: string): boolean {
|
|||||||
export function linkIsInternal(
|
export function linkIsInternal(
|
||||||
currentUrl: string,
|
currentUrl: string,
|
||||||
newUrl: string,
|
newUrl: string,
|
||||||
internalUrlRegex: string | RegExp,
|
internalUrlRegex: string | RegExp | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
log.debug('linkIsInternal', { currentUrl, newUrl, internalUrlRegex });
|
log.debug('linkIsInternal', { currentUrl, newUrl, internalUrlRegex });
|
||||||
if (newUrl.split('#')[0] === 'about:blank') {
|
if (newUrl.split('#')[0] === 'about:blank') {
|
||||||
|
@ -72,7 +72,7 @@ function findFlashOnMac(): string {
|
|||||||
)[0];
|
)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inferFlashPath(): string {
|
export function inferFlashPath(): string | undefined {
|
||||||
if (isOSX()) {
|
if (isOSX()) {
|
||||||
return findFlashOnMac();
|
return findFlashOnMac();
|
||||||
}
|
}
|
||||||
@ -86,5 +86,5 @@ export function inferFlashPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.warn('Unable to determine OS to infer flash player');
|
log.warn('Unable to determine OS to infer flash player');
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,33 @@ jest.mock('./windowEvents');
|
|||||||
jest.mock('./windowHelpers');
|
jest.mock('./windowHelpers');
|
||||||
|
|
||||||
import { dialog, BrowserWindow, WebContents } from 'electron';
|
import { dialog, BrowserWindow, WebContents } from 'electron';
|
||||||
|
import { WindowOptions } from '../../../shared/src/options/model';
|
||||||
import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers';
|
import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers';
|
||||||
const { onNewWindowHelper, onWillNavigate, onWillPreventUnload } =
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
jest.requireActual('./windowEvents');
|
const {
|
||||||
|
onNewWindowHelper,
|
||||||
|
onWillNavigate,
|
||||||
|
onWillPreventUnload,
|
||||||
|
}: {
|
||||||
|
onNewWindowHelper: (
|
||||||
|
options: WindowOptions,
|
||||||
|
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||||
|
urlToGo: string,
|
||||||
|
disposition: string | undefined,
|
||||||
|
preventDefault: (newGuest: BrowserWindow) => void,
|
||||||
|
parent?: BrowserWindow,
|
||||||
|
) => Promise<void>;
|
||||||
|
onWillNavigate: (
|
||||||
|
options: {
|
||||||
|
blockExternalUrls: boolean;
|
||||||
|
internalUrls?: string | RegExp;
|
||||||
|
targetUrl: string;
|
||||||
|
},
|
||||||
|
event: unknown,
|
||||||
|
urlToGo: string,
|
||||||
|
) => Promise<void>;
|
||||||
|
onWillPreventUnload: (event: unknown) => void;
|
||||||
|
} = jest.requireActual('./windowEvents');
|
||||||
import {
|
import {
|
||||||
blockExternalURL,
|
blockExternalURL,
|
||||||
createAboutBlankWindow,
|
createAboutBlankWindow,
|
||||||
@ -18,7 +42,13 @@ describe('onNewWindowHelper', () => {
|
|||||||
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
|
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
|
||||||
const foregroundDisposition = 'foreground-tab';
|
const foregroundDisposition = 'foreground-tab';
|
||||||
const backgroundDisposition = 'background-tab';
|
const backgroundDisposition = 'background-tab';
|
||||||
|
const baseOptions = {
|
||||||
|
blockExternalUrls: false,
|
||||||
|
insecure: false,
|
||||||
|
name: 'TEST_APP',
|
||||||
|
targetUrl: originalURL,
|
||||||
|
zoom: 1.0,
|
||||||
|
};
|
||||||
const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock;
|
const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock;
|
||||||
const mockCreateAboutBlank: jest.SpyInstance =
|
const mockCreateAboutBlank: jest.SpyInstance =
|
||||||
createAboutBlankWindow as jest.Mock;
|
createAboutBlankWindow as jest.Mock;
|
||||||
@ -54,14 +84,9 @@ describe('onNewWindowHelper', () => {
|
|||||||
mockOpenExternal.mockRestore();
|
mockOpenExternal.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('internal urls should not be handled', () => {
|
test('internal urls should not be handled', async () => {
|
||||||
const options = {
|
await onNewWindowHelper(
|
||||||
blockExternalUrls: false,
|
baseOptions,
|
||||||
targetUrl: originalURL,
|
|
||||||
};
|
|
||||||
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
internalURL,
|
internalURL,
|
||||||
undefined,
|
undefined,
|
||||||
@ -75,14 +100,11 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).not.toHaveBeenCalled();
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('external urls should be opened externally', () => {
|
test('external urls should be opened externally', async () => {
|
||||||
mockLinkIsInternal.mockReturnValue(false);
|
mockLinkIsInternal.mockReturnValue(false);
|
||||||
const options = {
|
|
||||||
blockExternalUrls: false,
|
await onNewWindowHelper(
|
||||||
targetUrl: originalURL,
|
baseOptions,
|
||||||
};
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
externalURL,
|
externalURL,
|
||||||
undefined,
|
undefined,
|
||||||
@ -96,13 +118,13 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('external urls should be ignored if blockExternalUrls is true', () => {
|
test('external urls should be ignored if blockExternalUrls is true', async () => {
|
||||||
mockLinkIsInternal.mockReturnValue(false);
|
mockLinkIsInternal.mockReturnValue(false);
|
||||||
const options = {
|
const options = {
|
||||||
|
...baseOptions,
|
||||||
blockExternalUrls: true,
|
blockExternalUrls: true,
|
||||||
targetUrl: originalURL,
|
|
||||||
};
|
};
|
||||||
onNewWindowHelper(
|
await onNewWindowHelper(
|
||||||
options,
|
options,
|
||||||
setupWindow,
|
setupWindow,
|
||||||
externalURL,
|
externalURL,
|
||||||
@ -117,13 +139,9 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tab disposition should be ignored if tabs are not enabled', () => {
|
test('tab disposition should be ignored if tabs are not enabled', async () => {
|
||||||
const options = {
|
await onNewWindowHelper(
|
||||||
blockExternalUrls: false,
|
baseOptions,
|
||||||
targetUrl: originalURL,
|
|
||||||
};
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
internalURL,
|
internalURL,
|
||||||
foregroundDisposition,
|
foregroundDisposition,
|
||||||
@ -137,14 +155,11 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).not.toHaveBeenCalled();
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tab disposition should be ignored if url is external', () => {
|
test('tab disposition should be ignored if url is external', async () => {
|
||||||
mockLinkIsInternal.mockReturnValue(false);
|
mockLinkIsInternal.mockReturnValue(false);
|
||||||
const options = {
|
|
||||||
blockExternalUrls: false,
|
await onNewWindowHelper(
|
||||||
targetUrl: originalURL,
|
baseOptions,
|
||||||
};
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
externalURL,
|
externalURL,
|
||||||
foregroundDisposition,
|
foregroundDisposition,
|
||||||
@ -158,15 +173,11 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('foreground tabs with internal urls should be opened in the foreground', () => {
|
test('foreground tabs with internal urls should be opened in the foreground', async () => {
|
||||||
mockNativeTabsSupported.mockReturnValue(true);
|
mockNativeTabsSupported.mockReturnValue(true);
|
||||||
|
|
||||||
const options = {
|
await onNewWindowHelper(
|
||||||
blockExternalUrls: false,
|
baseOptions,
|
||||||
targetUrl: originalURL,
|
|
||||||
};
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
internalURL,
|
internalURL,
|
||||||
foregroundDisposition,
|
foregroundDisposition,
|
||||||
@ -176,7 +187,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||||
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
||||||
expect(mockCreateNewTab).toHaveBeenCalledWith(
|
expect(mockCreateNewTab).toHaveBeenCalledWith(
|
||||||
options,
|
baseOptions,
|
||||||
setupWindow,
|
setupWindow,
|
||||||
internalURL,
|
internalURL,
|
||||||
true,
|
true,
|
||||||
@ -187,15 +198,11 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('background tabs with internal urls should be opened in background tabs', () => {
|
test('background tabs with internal urls should be opened in background tabs', async () => {
|
||||||
mockNativeTabsSupported.mockReturnValue(true);
|
mockNativeTabsSupported.mockReturnValue(true);
|
||||||
|
|
||||||
const options = {
|
await onNewWindowHelper(
|
||||||
blockExternalUrls: false,
|
baseOptions,
|
||||||
targetUrl: originalURL,
|
|
||||||
};
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
internalURL,
|
internalURL,
|
||||||
backgroundDisposition,
|
backgroundDisposition,
|
||||||
@ -205,7 +212,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||||
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
||||||
expect(mockCreateNewTab).toHaveBeenCalledWith(
|
expect(mockCreateNewTab).toHaveBeenCalledWith(
|
||||||
options,
|
baseOptions,
|
||||||
setupWindow,
|
setupWindow,
|
||||||
internalURL,
|
internalURL,
|
||||||
false,
|
false,
|
||||||
@ -216,13 +223,9 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('about:blank urls should be handled', () => {
|
test('about:blank urls should be handled', async () => {
|
||||||
const options = {
|
await onNewWindowHelper(
|
||||||
blockExternalUrls: false,
|
baseOptions,
|
||||||
targetUrl: originalURL,
|
|
||||||
};
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
'about:blank',
|
'about:blank',
|
||||||
undefined,
|
undefined,
|
||||||
@ -236,13 +239,9 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('about:blank#blocked urls should be handled', () => {
|
test('about:blank#blocked urls should be handled', async () => {
|
||||||
const options = {
|
await onNewWindowHelper(
|
||||||
blockExternalUrls: false,
|
baseOptions,
|
||||||
targetUrl: originalURL,
|
|
||||||
};
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
'about:blank#blocked',
|
'about:blank#blocked',
|
||||||
undefined,
|
undefined,
|
||||||
@ -256,13 +255,9 @@ describe('onNewWindowHelper', () => {
|
|||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('about:blank#other urls should not be handled', () => {
|
test('about:blank#other urls should not be handled', async () => {
|
||||||
const options = {
|
await onNewWindowHelper(
|
||||||
blockExternalUrls: false,
|
baseOptions,
|
||||||
targetUrl: originalURL,
|
|
||||||
};
|
|
||||||
onNewWindowHelper(
|
|
||||||
options,
|
|
||||||
setupWindow,
|
setupWindow,
|
||||||
'about:blank#other',
|
'about:blank#other',
|
||||||
undefined,
|
undefined,
|
||||||
@ -302,40 +297,40 @@ describe('onWillNavigate', () => {
|
|||||||
mockOpenExternal.mockRestore();
|
mockOpenExternal.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('internal urls should not be handled', () => {
|
test('internal urls should not be handled', async () => {
|
||||||
mockLinkIsInternal.mockReturnValue(true);
|
mockLinkIsInternal.mockReturnValue(true);
|
||||||
const options = {
|
const options = {
|
||||||
blockExternalUrls: false,
|
blockExternalUrls: false,
|
||||||
targetUrl: originalURL,
|
targetUrl: originalURL,
|
||||||
};
|
};
|
||||||
const event = { preventDefault };
|
const event = { preventDefault };
|
||||||
onWillNavigate(options, event, internalURL);
|
await onWillNavigate(options, event, internalURL);
|
||||||
|
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).not.toHaveBeenCalled();
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('external urls should be opened externally', () => {
|
test('external urls should be opened externally', async () => {
|
||||||
const options = {
|
const options = {
|
||||||
blockExternalUrls: false,
|
blockExternalUrls: false,
|
||||||
targetUrl: originalURL,
|
targetUrl: originalURL,
|
||||||
};
|
};
|
||||||
const event = { preventDefault };
|
const event = { preventDefault };
|
||||||
onWillNavigate(options, event, externalURL);
|
await onWillNavigate(options, event, externalURL);
|
||||||
|
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('external urls should be ignored if blockExternalUrls is true', () => {
|
test('external urls should be ignored if blockExternalUrls is true', async () => {
|
||||||
const options = {
|
const options = {
|
||||||
blockExternalUrls: true,
|
blockExternalUrls: true,
|
||||||
targetUrl: originalURL,
|
targetUrl: originalURL,
|
||||||
};
|
};
|
||||||
const event = { preventDefault };
|
const event = { preventDefault };
|
||||||
onWillNavigate(options, event, externalURL);
|
await onWillNavigate(options, event, externalURL);
|
||||||
|
|
||||||
expect(mockBlockExternalURL).toHaveBeenCalledTimes(1);
|
expect(mockBlockExternalURL).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
dialog,
|
dialog,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
IpcMainEvent,
|
Event,
|
||||||
NewWindowWebContentsEvent,
|
NewWindowWebContentsEvent,
|
||||||
WebContents,
|
WebContents,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
import { WindowOptions } from '../../../shared/src/options/model';
|
||||||
|
|
||||||
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
|
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
|
||||||
import {
|
import {
|
||||||
@ -18,8 +19,8 @@ import {
|
|||||||
} from './windowHelpers';
|
} from './windowHelpers';
|
||||||
|
|
||||||
export function onNewWindow(
|
export function onNewWindow(
|
||||||
options,
|
options: WindowOptions,
|
||||||
setupWindow: (...args) => void,
|
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||||
event: NewWindowWebContentsEvent,
|
event: NewWindowWebContentsEvent,
|
||||||
urlToGo: string,
|
urlToGo: string,
|
||||||
frameName: string,
|
frameName: string,
|
||||||
@ -39,7 +40,7 @@ export function onNewWindow(
|
|||||||
disposition,
|
disposition,
|
||||||
parent,
|
parent,
|
||||||
});
|
});
|
||||||
const preventDefault = (newGuest: BrowserWindow): void => {
|
const preventDefault = (newGuest?: BrowserWindow): void => {
|
||||||
log.debug('onNewWindow.preventDefault', { newGuest, event });
|
log.debug('onNewWindow.preventDefault', { newGuest, event });
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (newGuest) {
|
if (newGuest) {
|
||||||
@ -57,14 +58,15 @@ export function onNewWindow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function onNewWindowHelper(
|
export function onNewWindowHelper(
|
||||||
options,
|
options: WindowOptions,
|
||||||
setupWindow: (...args) => void,
|
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||||
urlToGo: string,
|
urlToGo: string,
|
||||||
disposition: string,
|
disposition: string | undefined,
|
||||||
preventDefault,
|
preventDefault: (newGuest?: BrowserWindow) => void,
|
||||||
parent?: BrowserWindow,
|
parent?: BrowserWindow,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
log.debug('onNewWindowHelper', {
|
log.debug('onNewWindowHelper', {
|
||||||
|
options,
|
||||||
urlToGo,
|
urlToGo,
|
||||||
disposition,
|
disposition,
|
||||||
preventDefault,
|
preventDefault,
|
||||||
@ -74,7 +76,13 @@ export function onNewWindowHelper(
|
|||||||
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
|
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
|
||||||
preventDefault();
|
preventDefault();
|
||||||
if (options.blockExternalUrls) {
|
if (options.blockExternalUrls) {
|
||||||
return blockExternalURL(urlToGo).then(() => null);
|
return new Promise((resolve) => {
|
||||||
|
blockExternalURL(urlToGo)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return openExternal(urlToGo);
|
return openExternal(urlToGo);
|
||||||
}
|
}
|
||||||
@ -108,15 +116,21 @@ export function onNewWindowHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function onWillNavigate(
|
export function onWillNavigate(
|
||||||
options,
|
options: WindowOptions,
|
||||||
event: IpcMainEvent,
|
event: Event,
|
||||||
urlToGo: string,
|
urlToGo: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
log.debug('onWillNavigate', { options, event, urlToGo });
|
log.debug('onWillNavigate', { options, event, urlToGo });
|
||||||
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
|
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (options.blockExternalUrls) {
|
if (options.blockExternalUrls) {
|
||||||
return blockExternalURL(urlToGo).then(() => null);
|
return new Promise((resolve) => {
|
||||||
|
blockExternalURL(urlToGo)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return openExternal(urlToGo);
|
return openExternal(urlToGo);
|
||||||
}
|
}
|
||||||
@ -124,29 +138,39 @@ export function onWillNavigate(
|
|||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onWillPreventUnload(event: IpcMainEvent): void {
|
export function onWillPreventUnload(
|
||||||
|
event: Event & { sender?: WebContents },
|
||||||
|
): void {
|
||||||
log.debug('onWillPreventUnload', event);
|
log.debug('onWillPreventUnload', event);
|
||||||
|
|
||||||
const webContents: WebContents = event.sender;
|
const webContents = event.sender;
|
||||||
if (webContents === undefined) {
|
if (!webContents) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const browserWindow = BrowserWindow.fromWebContents(webContents);
|
const browserWindow =
|
||||||
const choice = dialog.showMessageBoxSync(browserWindow, {
|
BrowserWindow.fromWebContents(webContents) ??
|
||||||
type: 'question',
|
BrowserWindow.getFocusedWindow();
|
||||||
buttons: ['Proceed', 'Stay'],
|
if (browserWindow) {
|
||||||
message: 'You may have unsaved changes, are you sure you want to proceed?',
|
const choice = dialog.showMessageBoxSync(browserWindow, {
|
||||||
title: 'Changes you made may not be saved.',
|
type: 'question',
|
||||||
defaultId: 0,
|
buttons: ['Proceed', 'Stay'],
|
||||||
cancelId: 1,
|
message:
|
||||||
});
|
'You may have unsaved changes, are you sure you want to proceed?',
|
||||||
if (choice === 0) {
|
title: 'Changes you made may not be saved.',
|
||||||
event.preventDefault();
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
if (choice === 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupNativefierWindow(options, window: BrowserWindow): void {
|
export function setupNativefierWindow(
|
||||||
|
options: WindowOptions,
|
||||||
|
window: BrowserWindow,
|
||||||
|
): void {
|
||||||
if (options.userAgent) {
|
if (options.userAgent) {
|
||||||
window.webContents.userAgent = options.userAgent;
|
window.webContents.userAgent = options.userAgent;
|
||||||
}
|
}
|
||||||
@ -157,7 +181,7 @@ export function setupNativefierWindow(options, window: BrowserWindow): void {
|
|||||||
|
|
||||||
injectCSS(window);
|
injectCSS(window);
|
||||||
|
|
||||||
window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => {
|
window.webContents.on('will-navigate', (event: Event, url: string) => {
|
||||||
onWillNavigate(options, event, url).catch((err) => {
|
onWillNavigate(options, event, url).catch((err) => {
|
||||||
log.error('window.webContents.on.will-navigate ERROR', err);
|
log.error('window.webContents.on.will-navigate ERROR', err);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from 'electron';
|
} from 'electron';
|
||||||
jest.mock('loglevel');
|
jest.mock('loglevel');
|
||||||
import { error } from 'loglevel';
|
import { error } from 'loglevel';
|
||||||
|
import { WindowOptions } from '../../../shared/src/options/model';
|
||||||
|
|
||||||
jest.mock('./helpers');
|
jest.mock('./helpers');
|
||||||
import { getCSSToInject } from './helpers';
|
import { getCSSToInject } from './helpers';
|
||||||
@ -57,7 +58,13 @@ describe('clearAppData', () => {
|
|||||||
|
|
||||||
describe('createNewTab', () => {
|
describe('createNewTab', () => {
|
||||||
const window = new BrowserWindow();
|
const window = new BrowserWindow();
|
||||||
const options = {};
|
const options: WindowOptions = {
|
||||||
|
blockExternalUrls: false,
|
||||||
|
insecure: false,
|
||||||
|
name: 'Test App',
|
||||||
|
targetUrl: 'https://github.com/nativefier/natifefier',
|
||||||
|
zoom: 1.0,
|
||||||
|
};
|
||||||
const setupWindow = jest.fn();
|
const setupWindow = jest.fn();
|
||||||
const url = 'https://github.com/nativefier/nativefier';
|
const url = 'https://github.com/nativefier/nativefier';
|
||||||
const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn(
|
const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn(
|
||||||
@ -100,6 +107,7 @@ describe('injectCSS', () => {
|
|||||||
jest.setTimeout(10000);
|
jest.setTimeout(10000);
|
||||||
|
|
||||||
const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock;
|
const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock;
|
||||||
|
let mockGetURL: jest.SpyInstance;
|
||||||
const mockLogError: jest.SpyInstance = error as jest.Mock;
|
const mockLogError: jest.SpyInstance = error as jest.Mock;
|
||||||
const mockWebContentsInsertCSS: jest.SpyInstance = jest.spyOn(
|
const mockWebContentsInsertCSS: jest.SpyInstance = jest.spyOn(
|
||||||
WebContents.prototype,
|
WebContents.prototype,
|
||||||
@ -107,17 +115,21 @@ describe('injectCSS', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const css = 'body { color: white; }';
|
const css = 'body { color: white; }';
|
||||||
let responseHeaders;
|
let responseHeaders: Record<string, string[]>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGetCSSToInject.mockReset().mockReturnValue('');
|
mockGetCSSToInject.mockReset().mockReturnValue('');
|
||||||
|
mockGetURL = jest
|
||||||
|
.spyOn(WebContents.prototype, 'getURL')
|
||||||
|
.mockReturnValue('https://example.com');
|
||||||
mockLogError.mockReset();
|
mockLogError.mockReset();
|
||||||
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
||||||
responseHeaders = { 'x-header': 'value', 'content-type': ['test/other'] };
|
responseHeaders = { 'x-header': ['value'], 'content-type': ['test/other'] };
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
mockGetCSSToInject.mockRestore();
|
mockGetCSSToInject.mockRestore();
|
||||||
|
mockGetURL.mockRestore();
|
||||||
mockLogError.mockRestore();
|
mockLogError.mockRestore();
|
||||||
mockWebContentsInsertCSS.mockRestore();
|
mockWebContentsInsertCSS.mockRestore();
|
||||||
});
|
});
|
||||||
@ -141,6 +153,7 @@ describe('injectCSS', () => {
|
|||||||
|
|
||||||
window.webContents.emit('did-navigate');
|
window.webContents.emit('did-navigate');
|
||||||
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
window.webContents.session.webRequest.send(
|
window.webContents.session.webRequest.send(
|
||||||
'onHeadersReceived',
|
'onHeadersReceived',
|
||||||
{ responseHeaders, webContents: window.webContents },
|
{ responseHeaders, webContents: window.webContents },
|
||||||
@ -168,6 +181,7 @@ describe('injectCSS', () => {
|
|||||||
|
|
||||||
window.webContents.emit('did-navigate');
|
window.webContents.emit('did-navigate');
|
||||||
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
window.webContents.session.webRequest.send(
|
window.webContents.session.webRequest.send(
|
||||||
'onHeadersReceived',
|
'onHeadersReceived',
|
||||||
{ responseHeaders, webContents: window.webContents },
|
{ responseHeaders, webContents: window.webContents },
|
||||||
@ -190,6 +204,11 @@ describe('injectCSS', () => {
|
|||||||
'image/png',
|
'image/png',
|
||||||
])(
|
])(
|
||||||
'will not inject for content-type %s',
|
'will not inject for content-type %s',
|
||||||
|
// @ts-expect-error because TypeScript can't recognize that
|
||||||
|
// '(contentType: string, done: jest.DoneCallback) => void'
|
||||||
|
// and
|
||||||
|
// '(...args: (string | DoneCallback)[]) => any'
|
||||||
|
// are actually compatible.
|
||||||
(contentType: string, done: jest.DoneCallback) => {
|
(contentType: string, done: jest.DoneCallback) => {
|
||||||
mockGetCSSToInject.mockReturnValue(css);
|
mockGetCSSToInject.mockReturnValue(css);
|
||||||
const window = new BrowserWindow();
|
const window = new BrowserWindow();
|
||||||
@ -203,6 +222,7 @@ describe('injectCSS', () => {
|
|||||||
expect(window.webContents.emit('did-navigate')).toBe(true);
|
expect(window.webContents.emit('did-navigate')).toBe(true);
|
||||||
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
||||||
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
window.webContents.session.webRequest.send(
|
window.webContents.session.webRequest.send(
|
||||||
'onHeadersReceived',
|
'onHeadersReceived',
|
||||||
{
|
{
|
||||||
@ -223,6 +243,11 @@ describe('injectCSS', () => {
|
|||||||
|
|
||||||
test.each<string | jest.DoneCallback>(['text/html'])(
|
test.each<string | jest.DoneCallback>(['text/html'])(
|
||||||
'will inject for content-type %s',
|
'will inject for content-type %s',
|
||||||
|
// @ts-expect-error because TypeScript can't recognize that
|
||||||
|
// '(contentType: string, done: jest.DoneCallback) => void'
|
||||||
|
// and
|
||||||
|
// '(...args: (string | DoneCallback)[]) => any'
|
||||||
|
// are actually compatible.
|
||||||
(contentType: string, done: jest.DoneCallback) => {
|
(contentType: string, done: jest.DoneCallback) => {
|
||||||
mockGetCSSToInject.mockReturnValue(css);
|
mockGetCSSToInject.mockReturnValue(css);
|
||||||
const window = new BrowserWindow();
|
const window = new BrowserWindow();
|
||||||
@ -236,6 +261,7 @@ describe('injectCSS', () => {
|
|||||||
window.webContents.emit('did-navigate');
|
window.webContents.emit('did-navigate');
|
||||||
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
||||||
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
window.webContents.session.webRequest.send(
|
window.webContents.session.webRequest.send(
|
||||||
'onHeadersReceived',
|
'onHeadersReceived',
|
||||||
{
|
{
|
||||||
@ -260,6 +286,11 @@ describe('injectCSS', () => {
|
|||||||
'xhr',
|
'xhr',
|
||||||
])(
|
])(
|
||||||
'will not inject for resource type %s',
|
'will not inject for resource type %s',
|
||||||
|
// @ts-expect-error because TypeScript can't recognize that
|
||||||
|
// '(contentType: string, done: jest.DoneCallback) => void'
|
||||||
|
// and
|
||||||
|
// '(...args: (string | DoneCallback)[]) => any'
|
||||||
|
// are actually compatible.
|
||||||
(resourceType: string, done: jest.DoneCallback) => {
|
(resourceType: string, done: jest.DoneCallback) => {
|
||||||
mockGetCSSToInject.mockReturnValue(css);
|
mockGetCSSToInject.mockReturnValue(css);
|
||||||
const window = new BrowserWindow();
|
const window = new BrowserWindow();
|
||||||
@ -271,6 +302,7 @@ describe('injectCSS', () => {
|
|||||||
window.webContents.emit('did-navigate');
|
window.webContents.emit('did-navigate');
|
||||||
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
||||||
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
window.webContents.session.webRequest.send(
|
window.webContents.session.webRequest.send(
|
||||||
'onHeadersReceived',
|
'onHeadersReceived',
|
||||||
{
|
{
|
||||||
@ -292,6 +324,11 @@ describe('injectCSS', () => {
|
|||||||
|
|
||||||
test.each<string | jest.DoneCallback>(['html', 'other'])(
|
test.each<string | jest.DoneCallback>(['html', 'other'])(
|
||||||
'will inject for resource type %s',
|
'will inject for resource type %s',
|
||||||
|
// @ts-expect-error because TypeScript can't recognize that
|
||||||
|
// '(contentType: string, done: jest.DoneCallback) => void'
|
||||||
|
// and
|
||||||
|
// '(...args: (string | DoneCallback)[]) => any'
|
||||||
|
// are actually compatible.
|
||||||
(resourceType: string, done: jest.DoneCallback) => {
|
(resourceType: string, done: jest.DoneCallback) => {
|
||||||
mockGetCSSToInject.mockReturnValue(css);
|
mockGetCSSToInject.mockReturnValue(css);
|
||||||
const window = new BrowserWindow();
|
const window = new BrowserWindow();
|
||||||
@ -303,6 +340,7 @@ describe('injectCSS', () => {
|
|||||||
window.webContents.emit('did-navigate');
|
window.webContents.emit('did-navigate');
|
||||||
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
|
||||||
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
window.webContents.session.webRequest.send(
|
window.webContents.session.webRequest.send(
|
||||||
'onHeadersReceived',
|
'onHeadersReceived',
|
||||||
{
|
{
|
||||||
|
@ -2,14 +2,16 @@ import {
|
|||||||
dialog,
|
dialog,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
BrowserWindowConstructorOptions,
|
BrowserWindowConstructorOptions,
|
||||||
|
Event,
|
||||||
HeadersReceivedResponse,
|
HeadersReceivedResponse,
|
||||||
IpcMainEvent,
|
|
||||||
MessageBoxReturnValue,
|
MessageBoxReturnValue,
|
||||||
OnHeadersReceivedListenerDetails,
|
OnHeadersReceivedListenerDetails,
|
||||||
|
WebPreferences,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
|
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { TrayValue, WindowOptions } from '../../../shared/src/options/model';
|
||||||
import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers';
|
import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers';
|
||||||
|
|
||||||
const ZOOM_INTERVAL = 0.1;
|
const ZOOM_INTERVAL = 0.1;
|
||||||
@ -61,8 +63,8 @@ export async function clearCache(window: BrowserWindow): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createAboutBlankWindow(
|
export function createAboutBlankWindow(
|
||||||
options,
|
options: WindowOptions,
|
||||||
setupWindow: (...args) => void,
|
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||||
parent?: BrowserWindow,
|
parent?: BrowserWindow,
|
||||||
): BrowserWindow {
|
): BrowserWindow {
|
||||||
const window = createNewWindow(options, setupWindow, 'about:blank', parent);
|
const window = createNewWindow(options, setupWindow, 'about:blank', parent);
|
||||||
@ -78,12 +80,12 @@ export function createAboutBlankWindow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createNewTab(
|
export function createNewTab(
|
||||||
options,
|
options: WindowOptions,
|
||||||
setupWindow,
|
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||||
url: string,
|
url: string,
|
||||||
foreground: boolean,
|
foreground: boolean,
|
||||||
parent?: BrowserWindow,
|
parent?: BrowserWindow,
|
||||||
): BrowserWindow {
|
): BrowserWindow | undefined {
|
||||||
log.debug('createNewTab', { url, foreground, parent });
|
log.debug('createNewTab', { url, foreground, parent });
|
||||||
return withFocusedWindow((focusedWindow) => {
|
return withFocusedWindow((focusedWindow) => {
|
||||||
const newTab = createNewWindow(options, setupWindow, url, parent);
|
const newTab = createNewWindow(options, setupWindow, url, parent);
|
||||||
@ -96,8 +98,8 @@ export function createNewTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createNewWindow(
|
export function createNewWindow(
|
||||||
options,
|
options: WindowOptions,
|
||||||
setupWindow: (...args) => void,
|
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||||
url: string,
|
url: string,
|
||||||
parent?: BrowserWindow,
|
parent?: BrowserWindow,
|
||||||
): BrowserWindow {
|
): BrowserWindow {
|
||||||
@ -118,7 +120,7 @@ export function getCurrentURL(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultWindowOptions(
|
export function getDefaultWindowOptions(
|
||||||
options,
|
options: WindowOptions,
|
||||||
): BrowserWindowConstructorOptions {
|
): BrowserWindowConstructorOptions {
|
||||||
const browserwindowOptions: BrowserWindowConstructorOptions = {
|
const browserwindowOptions: BrowserWindowConstructorOptions = {
|
||||||
...options.browserwindowOptions,
|
...options.browserwindowOptions,
|
||||||
@ -128,7 +130,7 @@ export function getDefaultWindowOptions(
|
|||||||
// webPreferences specified in the DEFAULT_WINDOW_OPTIONS with itself
|
// webPreferences specified in the DEFAULT_WINDOW_OPTIONS with itself
|
||||||
delete browserwindowOptions.webPreferences;
|
delete browserwindowOptions.webPreferences;
|
||||||
|
|
||||||
const webPreferences = {
|
const webPreferences: WebPreferences = {
|
||||||
...(options.browserwindowOptions?.webPreferences ?? {}),
|
...(options.browserwindowOptions?.webPreferences ?? {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -171,15 +173,15 @@ export function goForward(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function goToURL(url: string): Promise<void> {
|
export function goToURL(url: string): Promise<void> | undefined {
|
||||||
return withFocusedWindow((focusedWindow) => focusedWindow.loadURL(url));
|
return withFocusedWindow((focusedWindow) => focusedWindow.loadURL(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hideWindow(
|
export function hideWindow(
|
||||||
window: BrowserWindow,
|
window: BrowserWindow,
|
||||||
event: IpcMainEvent,
|
event: Event,
|
||||||
fastQuit: boolean,
|
fastQuit: boolean,
|
||||||
tray: 'true' | 'false' | 'start-in-tray',
|
tray: TrayValue,
|
||||||
): void {
|
): void {
|
||||||
if (isOSX() && !fastQuit) {
|
if (isOSX() && !fastQuit) {
|
||||||
// this is called when exiting from clicking the cross button on the window
|
// this is called when exiting from clicking the cross button on the window
|
||||||
@ -221,7 +223,7 @@ export function injectCSS(browserWindow: BrowserWindow): void {
|
|||||||
callback: (headersReceivedResponse: HeadersReceivedResponse) => void,
|
callback: (headersReceivedResponse: HeadersReceivedResponse) => void,
|
||||||
) => {
|
) => {
|
||||||
const contentType =
|
const contentType =
|
||||||
'content-type' in details.responseHeaders
|
details.responseHeaders && 'content-type' in details.responseHeaders
|
||||||
? details.responseHeaders['content-type'][0]
|
? details.responseHeaders['content-type'][0]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@ -252,9 +254,9 @@ export function injectCSS(browserWindow: BrowserWindow): void {
|
|||||||
|
|
||||||
async function injectCSSIntoResponse(
|
async function injectCSSIntoResponse(
|
||||||
details: OnHeadersReceivedListenerDetails,
|
details: OnHeadersReceivedListenerDetails,
|
||||||
contentType: string,
|
contentType: string | undefined,
|
||||||
cssToInject: string,
|
cssToInject: string,
|
||||||
): Promise<Record<string, string[]>> {
|
): Promise<Record<string, string[]> | undefined> {
|
||||||
// We go with a denylist rather than a whitelist (e.g. only text/html)
|
// We go with a denylist rather than a whitelist (e.g. only text/html)
|
||||||
// to avoid "whoops I didn't think this should have been CSS-injected" cases
|
// to avoid "whoops I didn't think this should have been CSS-injected" cases
|
||||||
const nonInjectableContentTypes = [
|
const nonInjectableContentTypes = [
|
||||||
@ -265,19 +267,26 @@ async function injectCSSIntoResponse(
|
|||||||
const nonInjectableResourceTypes = ['image', 'script', 'stylesheet', 'xhr'];
|
const nonInjectableResourceTypes = ['image', 'script', 'stylesheet', 'xhr'];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nonInjectableContentTypes.filter((x) => x.exec(contentType)?.length > 0)
|
(contentType &&
|
||||||
?.length > 0 ||
|
nonInjectableContentTypes.filter((x) => {
|
||||||
|
const matches = x.exec(contentType);
|
||||||
|
return matches && matches?.length > 0;
|
||||||
|
})?.length > 0) ||
|
||||||
nonInjectableResourceTypes.includes(details.resourceType) ||
|
nonInjectableResourceTypes.includes(details.resourceType) ||
|
||||||
!details.webContents
|
!details.webContents
|
||||||
) {
|
) {
|
||||||
log.debug(
|
log.debug(
|
||||||
`Skipping CSS injection for:\n${details.url}\nwith resourceType ${details.resourceType} and content-type ${contentType}`,
|
`Skipping CSS injection for:\n${details.url}\nwith resourceType ${
|
||||||
|
details.resourceType
|
||||||
|
} and content-type ${contentType as string}`,
|
||||||
);
|
);
|
||||||
return details.responseHeaders;
|
return details.responseHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
`Injecting CSS for:\n${details.url}\nwith resourceType ${details.resourceType} and content-type ${contentType}`,
|
`Injecting CSS for:\n${details.url}\nwith resourceType ${
|
||||||
|
details.resourceType
|
||||||
|
} and content-type ${contentType as string}`,
|
||||||
);
|
);
|
||||||
await details.webContents.insertCSS(cssToInject);
|
await details.webContents.insertCSS(cssToInject);
|
||||||
|
|
||||||
@ -285,7 +294,7 @@ async function injectCSSIntoResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sendParamsOnDidFinishLoad(
|
export function sendParamsOnDidFinishLoad(
|
||||||
options,
|
options: WindowOptions,
|
||||||
window: BrowserWindow,
|
window: BrowserWindow,
|
||||||
): void {
|
): void {
|
||||||
window.webContents.on('did-finish-load', () => {
|
window.webContents.on('did-finish-load', () => {
|
||||||
@ -304,7 +313,10 @@ export function sendParamsOnDidFinishLoad(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setProxyRules(window: BrowserWindow, proxyRules): void {
|
export function setProxyRules(
|
||||||
|
window: BrowserWindow,
|
||||||
|
proxyRules?: string,
|
||||||
|
): void {
|
||||||
window.webContents.session
|
window.webContents.session
|
||||||
.setProxy({
|
.setProxy({
|
||||||
proxyRules,
|
proxyRules,
|
||||||
@ -314,13 +326,15 @@ export function setProxyRules(window: BrowserWindow, proxyRules): void {
|
|||||||
.catch((err) => log.error('session.setProxy ERROR', err));
|
.catch((err) => log.error('session.setProxy ERROR', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withFocusedWindow<T>(block: (window: BrowserWindow) => T): T {
|
export function withFocusedWindow<T>(
|
||||||
|
block: (window: BrowserWindow) => T,
|
||||||
|
): T | undefined {
|
||||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||||
if (focusedWindow) {
|
if (focusedWindow) {
|
||||||
return block(focusedWindow);
|
return block(focusedWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function zoomOut(): void {
|
export function zoomOut(): void {
|
||||||
@ -328,7 +342,7 @@ export function zoomOut(): void {
|
|||||||
adjustWindowZoom(-ZOOM_INTERVAL);
|
adjustWindowZoom(-ZOOM_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function zoomReset(options): void {
|
export function zoomReset(options: { zoom?: number }): void {
|
||||||
log.debug('zoomReset');
|
log.debug('zoomReset');
|
||||||
withFocusedWindow((focusedWindow) => {
|
withFocusedWindow((focusedWindow) => {
|
||||||
focusedWindow.webContents.zoomFactor = options.zoom ?? 1.0;
|
focusedWindow.webContents.zoomFactor = options.zoom ?? 1.0;
|
||||||
|
128
app/src/main.ts
128
app/src/main.ts
@ -10,7 +10,7 @@ import electron, {
|
|||||||
globalShortcut,
|
globalShortcut,
|
||||||
systemPreferences,
|
systemPreferences,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
IpcMainEvent,
|
Event,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import electronDownload from 'electron-dl';
|
import electronDownload from 'electron-dl';
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
@ -25,6 +25,10 @@ import { createTrayIcon } from './components/trayIcon';
|
|||||||
import { isOSX, removeUserAgentSpecifics } from './helpers/helpers';
|
import { isOSX, removeUserAgentSpecifics } from './helpers/helpers';
|
||||||
import { inferFlashPath } from './helpers/inferFlash';
|
import { inferFlashPath } from './helpers/inferFlash';
|
||||||
import { setupNativefierWindow } from './helpers/windowEvents';
|
import { setupNativefierWindow } from './helpers/windowEvents';
|
||||||
|
import {
|
||||||
|
OutputOptions,
|
||||||
|
outputOptionsToWindowOptions,
|
||||||
|
} from '../../shared/src/options/model';
|
||||||
|
|
||||||
// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
|
// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
|
||||||
if (require('electron-squirrel-startup')) {
|
if (require('electron-squirrel-startup')) {
|
||||||
@ -39,7 +43,9 @@ if (process.argv.indexOf('--verbose') > -1) {
|
|||||||
|
|
||||||
let mainWindow: BrowserWindow;
|
let mainWindow: BrowserWindow;
|
||||||
|
|
||||||
const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'));
|
const appArgs = JSON.parse(
|
||||||
|
fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'),
|
||||||
|
) as OutputOptions;
|
||||||
|
|
||||||
log.debug('appArgs', appArgs);
|
log.debug('appArgs', appArgs);
|
||||||
// Do this relatively early so that we can start storing appData with the app
|
// Do this relatively early so that we can start storing appData with the app
|
||||||
@ -94,10 +100,13 @@ if (appArgs.processEnvs) {
|
|||||||
if (typeof appArgs.processEnvs === 'string') {
|
if (typeof appArgs.processEnvs === 'string') {
|
||||||
process.env.processEnvs = appArgs.processEnvs;
|
process.env.processEnvs = appArgs.processEnvs;
|
||||||
} else {
|
} else {
|
||||||
Object.keys(appArgs.processEnvs).forEach((key) => {
|
Object.keys(appArgs.processEnvs)
|
||||||
/* eslint-env node */
|
.filter((key) => key !== undefined)
|
||||||
process.env[key] = appArgs.processEnvs[key];
|
.forEach((key) => {
|
||||||
});
|
// @ts-expect-error TS will complain this could be undefined, but we filtered those out
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
process.env[key] = appArgs.processEnvs[key];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +134,10 @@ if (appArgs.enableEs3Apis) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (appArgs.diskCacheSize) {
|
if (appArgs.diskCacheSize) {
|
||||||
app.commandLine.appendSwitch('disk-cache-size', appArgs.diskCacheSize);
|
app.commandLine.appendSwitch(
|
||||||
|
'disk-cache-size',
|
||||||
|
appArgs.diskCacheSize.toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appArgs.basicAuthUsername) {
|
if (appArgs.basicAuthUsername) {
|
||||||
@ -143,7 +155,7 @@ if (appArgs.basicAuthPassword) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (appArgs.lang) {
|
if (appArgs.lang) {
|
||||||
const langParts = (appArgs.lang as string).split(',');
|
const langParts = appArgs.lang.split(',');
|
||||||
// Convert locales to languages, because for some reason locales don't work. Stupid Chromium
|
// Convert locales to languages, because for some reason locales don't work. Stupid Chromium
|
||||||
const langPartsParsed = Array.from(
|
const langPartsParsed = Array.from(
|
||||||
// Convert to set to dedupe in case something like "en-GB,en-US" was passed
|
// Convert to set to dedupe in case something like "en-GB,en-US" was passed
|
||||||
@ -156,10 +168,12 @@ if (appArgs.lang) {
|
|||||||
|
|
||||||
let currentBadgeCount = 0;
|
let currentBadgeCount = 0;
|
||||||
const setDockBadge = isOSX()
|
const setDockBadge = isOSX()
|
||||||
? (count: number, bounce = false): void => {
|
? (count?: number | string, bounce = false): void => {
|
||||||
app.dock.setBadge(count.toString());
|
if (count) {
|
||||||
if (bounce && count > currentBadgeCount) app.dock.bounce();
|
app.dock.setBadge(count.toString());
|
||||||
currentBadgeCount = count;
|
if (bounce && count > currentBadgeCount) app.dock.bounce();
|
||||||
|
currentBadgeCount = typeof count === 'number' ? count : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
: (): void => undefined;
|
: (): void => undefined;
|
||||||
|
|
||||||
@ -191,17 +205,17 @@ app.on('quit', (event, exitCode) => {
|
|||||||
log.debug('app.quit', { event, exitCode });
|
log.debug('app.quit', { event, exitCode });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appArgs.crashReporter) {
|
app.on('will-finish-launching', () => {
|
||||||
app.on('will-finish-launching', () => {
|
log.debug('app.will-finish-launching');
|
||||||
log.debug('app.will-finish-launching');
|
if (appArgs.crashReporter) {
|
||||||
crashReporter.start({
|
crashReporter.start({
|
||||||
companyName: appArgs.companyName || '',
|
companyName: appArgs.companyName ?? '',
|
||||||
productName: appArgs.name,
|
productName: appArgs.name,
|
||||||
submitURL: appArgs.crashReporter,
|
submitURL: appArgs.crashReporter,
|
||||||
uploadToServer: true,
|
uploadToServer: true,
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
if (appArgs.widevine) {
|
if (appArgs.widevine) {
|
||||||
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
|
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
|
||||||
@ -270,19 +284,28 @@ app.on('new-window-for-tab', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('login', (event, webContents, request, authInfo, callback) => {
|
app.on(
|
||||||
log.debug('app.login', { event, request });
|
'login',
|
||||||
// for http authentication
|
(
|
||||||
event.preventDefault();
|
event,
|
||||||
|
webContents,
|
||||||
|
request,
|
||||||
|
authInfo,
|
||||||
|
callback: (username?: string, password?: string) => void,
|
||||||
|
) => {
|
||||||
|
log.debug('app.login', { event, request });
|
||||||
|
// for http authentication
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
if (appArgs.basicAuthUsername && appArgs.basicAuthPassword) {
|
if (appArgs.basicAuthUsername && appArgs.basicAuthPassword) {
|
||||||
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
|
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
|
||||||
} else {
|
} else {
|
||||||
createLoginWindow(callback, mainWindow).catch((err) =>
|
createLoginWindow(callback, mainWindow).catch((err) =>
|
||||||
log.error('createLoginWindow ERROR', err),
|
log.error('createLoginWindow ERROR', err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
async function onReady(): Promise<void> {
|
async function onReady(): Promise<void> {
|
||||||
// Warning: `mainWindow` below is the *global* unique `mainWindow`, created at init time
|
// Warning: `mainWindow` below is the *global* unique `mainWindow`, created at init time
|
||||||
@ -295,6 +318,7 @@ async function onReady(): Promise<void> {
|
|||||||
appArgs.globalShortcuts.forEach((shortcut) => {
|
appArgs.globalShortcuts.forEach((shortcut) => {
|
||||||
globalShortcut.register(shortcut.key, () => {
|
globalShortcut.register(shortcut.key, () => {
|
||||||
shortcut.inputEvents.forEach((inputEvent) => {
|
shortcut.inputEvents.forEach((inputEvent) => {
|
||||||
|
// @ts-expect-error without including electron in our models, these will never match
|
||||||
mainWindow.webContents.sendInputEvent(inputEvent);
|
mainWindow.webContents.sendInputEvent(inputEvent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -319,16 +343,19 @@ async function onReady(): Promise<void> {
|
|||||||
// the user for permission on Mac.
|
// the user for permission on Mac.
|
||||||
// For reference:
|
// For reference:
|
||||||
// https://www.electronjs.org/docs/api/global-shortcut?q=MediaPlayPause#globalshortcutregisteraccelerator-callback
|
// https://www.electronjs.org/docs/api/global-shortcut?q=MediaPlayPause#globalshortcutregisteraccelerator-callback
|
||||||
const accessibilityPromptResult = dialog.showMessageBoxSync(null, {
|
const accessibilityPromptResult = dialog.showMessageBoxSync(
|
||||||
type: 'question',
|
mainWindow,
|
||||||
message: 'Accessibility Permissions Needed',
|
{
|
||||||
buttons: ['Yes', 'No', 'No and never ask again'],
|
type: 'question',
|
||||||
defaultId: 0,
|
message: 'Accessibility Permissions Needed',
|
||||||
detail:
|
buttons: ['Yes', 'No', 'No and never ask again'],
|
||||||
`${appArgs.name} would like to use one or more of your keyboard's media keys (start, stop, next track, or previous track) to control it.\n\n` +
|
defaultId: 0,
|
||||||
`Would you like Mac OS to ask for your permission to do so?\n\n` +
|
detail:
|
||||||
`If so, you will need to restart ${appArgs.name} after granting permissions for these keyboard shortcuts to begin working.`,
|
`${appArgs.name} would like to use one or more of your keyboard's media keys (start, stop, next track, or previous track) to control it.\n\n` +
|
||||||
});
|
`Would you like Mac OS to ask for your permission to do so?\n\n` +
|
||||||
|
`If so, you will need to restart ${appArgs.name} after granting permissions for these keyboard shortcuts to begin working.`,
|
||||||
|
},
|
||||||
|
);
|
||||||
switch (accessibilityPromptResult) {
|
switch (accessibilityPromptResult) {
|
||||||
// User clicked Yes, prompt for accessibility
|
// User clicked Yes, prompt for accessibility
|
||||||
case 0:
|
case 0:
|
||||||
@ -354,7 +381,7 @@ async function onReady(): Promise<void> {
|
|||||||
appArgs.oldBuildWarningText ||
|
appArgs.oldBuildWarningText ||
|
||||||
'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is insecure to keep using an old version of it. Please upgrade Nativefier and rebuild this app.';
|
'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is insecure to keep using an old version of it. Please upgrade Nativefier and rebuild this app.';
|
||||||
dialog
|
dialog
|
||||||
.showMessageBox(null, {
|
.showMessageBox(mainWindow, {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Old build detected',
|
message: 'Old build detected',
|
||||||
detail: oldBuildWarningText,
|
detail: oldBuildWarningText,
|
||||||
@ -365,7 +392,7 @@ async function onReady(): Promise<void> {
|
|||||||
|
|
||||||
app.on(
|
app.on(
|
||||||
'accessibility-support-changed',
|
'accessibility-support-changed',
|
||||||
(event: IpcMainEvent, accessibilitySupportEnabled: boolean) => {
|
(event: Event, accessibilitySupportEnabled: boolean) => {
|
||||||
log.debug('app.accessibility-support-changed', {
|
log.debug('app.accessibility-support-changed', {
|
||||||
event,
|
event,
|
||||||
accessibilitySupportEnabled,
|
accessibilitySupportEnabled,
|
||||||
@ -375,23 +402,20 @@ app.on(
|
|||||||
|
|
||||||
app.on(
|
app.on(
|
||||||
'activity-was-continued',
|
'activity-was-continued',
|
||||||
(event: IpcMainEvent, type: string, userInfo: unknown) => {
|
(event: Event, type: string, userInfo: unknown) => {
|
||||||
log.debug('app.activity-was-continued', { event, type, userInfo });
|
log.debug('app.activity-was-continued', { event, type, userInfo });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.on('browser-window-blur', (event: IpcMainEvent, window: BrowserWindow) => {
|
app.on('browser-window-blur', (event: Event, window: BrowserWindow) => {
|
||||||
log.debug('app.browser-window-blur', { event, window });
|
log.debug('app.browser-window-blur', { event, window });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on(
|
app.on('browser-window-created', (event: Event, window: BrowserWindow) => {
|
||||||
'browser-window-created',
|
log.debug('app.browser-window-created', { event, window });
|
||||||
(event: IpcMainEvent, window: BrowserWindow) => {
|
setupNativefierWindow(outputOptionsToWindowOptions(appArgs), window);
|
||||||
log.debug('app.browser-window-created', { event, window });
|
});
|
||||||
setupNativefierWindow(appArgs, window);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
app.on('browser-window-focus', (event: IpcMainEvent, window: BrowserWindow) => {
|
app.on('browser-window-focus', (event: Event, window: BrowserWindow) => {
|
||||||
log.debug('app.browser-window-focus', { event, window });
|
log.debug('app.browser-window-focus', { event, window });
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@ class MockBrowserWindow extends EventEmitter {
|
|||||||
webContents: MockWebContents;
|
webContents: MockWebContents;
|
||||||
|
|
||||||
constructor(options?: unknown) {
|
constructor(options?: unknown) {
|
||||||
|
// @ts-expect-error options is really EventEmitterOptions, but events.d.ts doesn't expose it...
|
||||||
super(options);
|
super(options);
|
||||||
this.webContents = new MockWebContents();
|
this.webContents = new MockWebContents();
|
||||||
}
|
}
|
||||||
@ -44,15 +45,15 @@ class MockBrowserWindow extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSimpleFullScreen(): boolean {
|
isSimpleFullScreen(): boolean {
|
||||||
return undefined;
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
isFullScreen(): boolean {
|
isFullScreen(): boolean {
|
||||||
return undefined;
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
isFullScreenable(): boolean {
|
isFullScreenable(): boolean {
|
||||||
return undefined;
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
loadURL(url: string, options?: unknown): Promise<void> {
|
loadURL(url: string, options?: unknown): Promise<void> {
|
||||||
@ -73,14 +74,14 @@ class MockDialog {
|
|||||||
browserWindow: MockBrowserWindow,
|
browserWindow: MockBrowserWindow,
|
||||||
options: unknown,
|
options: unknown,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
return Promise.resolve(undefined);
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
static showMessageBoxSync(
|
static showMessageBoxSync(
|
||||||
browserWindow: MockBrowserWindow,
|
browserWindow: MockBrowserWindow,
|
||||||
options: unknown,
|
options: unknown,
|
||||||
): number {
|
): number {
|
||||||
return undefined;
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,11 +111,11 @@ class MockWebContents extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getURL(): string {
|
getURL(): string {
|
||||||
return undefined;
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
insertCSS(css: string, options?: unknown): Promise<string> {
|
insertCSS(css: string, options?: unknown): Promise<string> {
|
||||||
return Promise.resolve(undefined);
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,13 +135,15 @@ class MockWebRequest {
|
|||||||
) => void)
|
) => void)
|
||||||
| null,
|
| null,
|
||||||
): void {
|
): void {
|
||||||
this.emitter.addListener(
|
if (listener) {
|
||||||
'onHeadersReceived',
|
this.emitter.addListener(
|
||||||
(
|
'onHeadersReceived',
|
||||||
details: unknown,
|
(
|
||||||
callback: (headersReceivedResponse: unknown) => void,
|
details: unknown,
|
||||||
) => listener(details, callback),
|
callback: (headersReceivedResponse: unknown) => void,
|
||||||
);
|
) => listener(details, callback),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
send(event: string, ...args: unknown[]): void {
|
send(event: string, ...args: unknown[]): void {
|
||||||
|
@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
import { OutputOptions } from '../../shared/src/options/model';
|
||||||
|
|
||||||
// Do *NOT* add 3rd-party imports here in preload (except for webpack `externals` like electron).
|
// Do *NOT* add 3rd-party imports here in preload (except for webpack `externals` like electron).
|
||||||
// They will work during development, but break in the prod build :-/ .
|
// They will work during development, but break in the prod build :-/ .
|
||||||
@ -56,7 +57,7 @@ function setNotificationCallback(
|
|||||||
get: () => OldNotify.permission,
|
get: () => OldNotify.permission,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error TypeScript says its not compatible, but it works?
|
||||||
window.Notification = newNotify;
|
window.Notification = newNotify;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,14 +93,15 @@ function notifyNotificationClick(): void {
|
|||||||
ipcRenderer.send('notification-click');
|
ipcRenderer.send('notification-click');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error TypeScript thinks these are incompatible but they aren't
|
||||||
setNotificationCallback(notifyNotificationCreate, notifyNotificationClick);
|
setNotificationCallback(notifyNotificationCreate, notifyNotificationClick);
|
||||||
|
|
||||||
ipcRenderer.on('params', (event, message) => {
|
ipcRenderer.on('params', (event, message: string) => {
|
||||||
log.debug('ipcRenderer.params', { event, message });
|
log.debug('ipcRenderer.params', { event, message });
|
||||||
const appArgs = JSON.parse(message);
|
const appArgs = JSON.parse(message) as OutputOptions;
|
||||||
log.info('nativefier.json', appArgs);
|
log.info('nativefier.json', appArgs);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('debug', (event, message) => {
|
ipcRenderer.on('debug', (event, message: string) => {
|
||||||
log.debug('ipcRenderer.debug', { event, message });
|
log.debug('ipcRenderer.debug', { event, message });
|
||||||
});
|
});
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../tsconfig-base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"outDir": "./dist",
|
||||||
"declaration": false,
|
// Here in app/tsconfig.json, we want to set the `target` and `lib` keys to
|
||||||
"esModuleInterop": true,
|
// the "best" values for the version of Node **coming with the chosen Electron**.
|
||||||
"incremental": true,
|
// Careful: we're *not* talking about Nativefier's (CLI) required Node version,
|
||||||
"module": "commonjs",
|
// we're talking about the version of the Node runtime **bundled with Electron**.
|
||||||
"moduleResolution": "node",
|
//
|
||||||
"outDir": "./dist",
|
// Like in our main tsconfig.json, we want to be as conservative as possible,
|
||||||
"resolveJsonModule": true,
|
// to support (as much as reasonable) users using old versions of Electron.
|
||||||
"skipLibCheck": true,
|
// Then, at some point, an app dependency (declared in app/package.json)
|
||||||
"sourceMap": true,
|
// will require a more recent Node, then it's okay to bump our app compilerOptions
|
||||||
// Here in app/tsconfig.json, we want to set the `target` and `lib` keys to
|
// to what's supported by the more recent Node.
|
||||||
// the "best" values for the version of Node **coming with the chosen Electron**.
|
//
|
||||||
// Careful: we're *not* talking about Nativefier's (CLI) required Node version,
|
// TS doesn't offer any easy "preset" for this, so the best we have is to
|
||||||
// we're talking about the version of the Node runtime **bundled with Electron**.
|
// believe people who know which {syntax, library} parts of current EcmaScript
|
||||||
//
|
// are supported for the version of Node coming with the Electron being used,
|
||||||
// Like in our main tsconfig.json, we want to be as conservative as possible,
|
// and use what they recommend. For the current Node version, I followed
|
||||||
// to support (as much as reasonable) users using old versions of Electron.
|
// https://stackoverflow.com/questions/51716406/typescript-tsconfig-settings-for-node-js-10
|
||||||
// Then, at some point, an app dependency (declared in app/package.json)
|
// and 'dom' to tell tsc it's okay to use the URL object (which is in Node >= 7)
|
||||||
// will require a more recent Node, then it's okay to bump our app compilerOptions
|
"target": "es2018",
|
||||||
// to what's supported by the more recent Node.
|
"lib": [
|
||||||
//
|
"es2018",
|
||||||
// TS doesn't offer any easy "preset" for this, so the best we have is to
|
"dom"
|
||||||
// believe people who know which {syntax, library} parts of current EcmaScript
|
]
|
||||||
// are supported for the version of Node coming with the Electron being used,
|
|
||||||
// and use what they recommend. For the current Node version, I followed
|
|
||||||
// https://stackoverflow.com/questions/51716406/typescript-tsconfig-settings-for-node-js-10
|
|
||||||
// and 'dom' to tell tsc it's okay to use the URL object (which is in Node >= 7)
|
|
||||||
"target": "es2018",
|
|
||||||
"lib": ["es2018", "dom"]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*"
|
"./src/**/*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../shared"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
|
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
project: ['./tsconfig.json'],
|
|
||||||
},
|
|
||||||
plugins: ['@typescript-eslint', 'prettier'],
|
plugins: ['@typescript-eslint', 'prettier'],
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
17
package.json
17
package.json
@ -33,15 +33,15 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build-app": "cd app && webpack",
|
"build-app": "cd app && webpack",
|
||||||
"build-app-static": "ncp app/src/static/ app/lib/static/ && ncp app/dist/preload.js app/lib/preload.js && ncp app/dist/preload.js.map app/lib/preload.js.map",
|
"build-app-static": "ncp app/src/static/ app/lib/static/ && ncp app/dist/preload.js app/lib/preload.js && ncp app/dist/preload.js.map app/lib/preload.js.map",
|
||||||
"build": "npm run clean && tsc --build . app && npm run build-app && npm run build-app-static",
|
"build": "npm run clean && tsc --build shared src app && npm run build-app && npm run build-app-static",
|
||||||
"build:watch": "npm run clean && tsc --build . app --watch",
|
"build:watch": "npm run clean && tsc --build . app --watch",
|
||||||
"changelog": "./.github/generate-changelog",
|
"changelog": "./.github/generate-changelog",
|
||||||
"ci": "npm run lint && npm test",
|
"ci": "npm run lint && npm test",
|
||||||
"clean": "rimraf coverage/ lib/ app/lib/ app/dist/",
|
"clean": "rimraf coverage/ lib/ app/lib/ app/dist/ shared/lib",
|
||||||
"clean:full": "rimraf coverage/ lib/ app/lib/ app/dist/ app/node_modules/ node_modules/",
|
"clean:full": "npm run clean && rimraf app/node_modules/ node_modules/",
|
||||||
"lint:fix": "eslint . --ext .ts --fix",
|
"lint:fix": "cd src && eslint . --ext .ts --fix && cd ../shared && eslint src --ext .ts --fix && cd ../app && eslint src --ext .ts --fix",
|
||||||
"lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts'",
|
"lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts' 'shared/src/**/*.ts'",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint shared app src --ext .ts",
|
||||||
"list-outdated-deps": "npm out; cd app && npm out; true",
|
"list-outdated-deps": "npm out; cd app && npm out; true",
|
||||||
"prepare": "cd app && npm install && cd .. && npm run build",
|
"prepare": "cd app && npm install && cd .. && npm run build",
|
||||||
"test:integration": "jest --testRegex '.*integration-test.js'",
|
"test:integration": "jest --testRegex '.*integration-test.js'",
|
||||||
@ -109,10 +109,11 @@
|
|||||||
],
|
],
|
||||||
"watchPathIgnorePatterns": [
|
"watchPathIgnorePatterns": [
|
||||||
"<rootDir>/src.*",
|
"<rootDir>/src.*",
|
||||||
"<rootDir>/tsconfig.json",
|
"<rootDir>/tsconfig-base.json",
|
||||||
"<rootDir>/app/src.*",
|
"<rootDir>/app/src.*",
|
||||||
"<rootDir>/app/lib.*",
|
"<rootDir>/app/lib.*",
|
||||||
"<rootDir>/app/tsconfig.json"
|
"<rootDir>/app/tsconfig.json",
|
||||||
|
"<rootDir>/shared/tsconfig.json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
14
shared/.eslintrc.js
Normal file
14
shared/.eslintrc.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const base = require('../base-eslintrc');
|
||||||
|
|
||||||
|
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
|
||||||
|
module.exports = {
|
||||||
|
parser: base.parser,
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
plugins: base.plugins,
|
||||||
|
extends: base.extends,
|
||||||
|
rules: base.rules,
|
||||||
|
ignorePatterns: ['lib/**'],
|
||||||
|
};
|
@ -1,6 +1,11 @@
|
|||||||
import { CreateOptions } from 'asar';
|
import { CreateOptions } from 'asar';
|
||||||
import * as electronPackager from 'electron-packager';
|
import * as electronPackager from 'electron-packager';
|
||||||
|
|
||||||
|
export type TitleBarValue =
|
||||||
|
| 'default'
|
||||||
|
| 'hidden'
|
||||||
|
| 'hiddenInset'
|
||||||
|
| 'customButtonsOnHover';
|
||||||
export type TrayValue = 'true' | 'false' | 'start-in-tray';
|
export type TrayValue = 'true' | 'false' | 'start-in-tray';
|
||||||
|
|
||||||
export interface ElectronPackagerOptions extends electronPackager.Options {
|
export interface ElectronPackagerOptions extends electronPackager.Options {
|
||||||
@ -34,7 +39,7 @@ export interface AppOptions {
|
|||||||
electronVersionUsed?: string;
|
electronVersionUsed?: string;
|
||||||
enableEs3Apis: boolean;
|
enableEs3Apis: boolean;
|
||||||
fastQuit: boolean;
|
fastQuit: boolean;
|
||||||
fileDownloadOptions: unknown;
|
fileDownloadOptions?: Record<string, unknown>;
|
||||||
flashPluginDir?: string;
|
flashPluginDir?: string;
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
globalShortcuts?: GlobalShortcut[];
|
globalShortcuts?: GlobalShortcut[];
|
||||||
@ -46,12 +51,12 @@ export interface AppOptions {
|
|||||||
internalUrls?: string;
|
internalUrls?: string;
|
||||||
lang?: string;
|
lang?: string;
|
||||||
maximize: boolean;
|
maximize: boolean;
|
||||||
nativefierVersion?: string;
|
nativefierVersion: string;
|
||||||
processEnvs?: string;
|
processEnvs?: string;
|
||||||
proxyRules?: string;
|
proxyRules?: string;
|
||||||
showMenuBar: boolean;
|
showMenuBar: boolean;
|
||||||
singleInstance: boolean;
|
singleInstance: boolean;
|
||||||
titleBarStyle?: string;
|
titleBarStyle?: TitleBarValue;
|
||||||
tray: TrayValue;
|
tray: TrayValue;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
userAgentHonest: boolean;
|
userAgentHonest: boolean;
|
||||||
@ -70,10 +75,26 @@ export interface AppOptions {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BrowserWindowOptions = Record<string, unknown>;
|
export type BrowserWindowOptions = Record<string, unknown> & {
|
||||||
|
webPreferences?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
export type GlobalShortcut = {
|
export type GlobalShortcut = {
|
||||||
key: string;
|
key: string;
|
||||||
|
inputEvents: {
|
||||||
|
type:
|
||||||
|
| 'mouseDown'
|
||||||
|
| 'mouseUp'
|
||||||
|
| 'mouseEnter'
|
||||||
|
| 'mouseLeave'
|
||||||
|
| 'contextMenu'
|
||||||
|
| 'mouseWheel'
|
||||||
|
| 'mouseMove'
|
||||||
|
| 'keyDown'
|
||||||
|
| 'keyUp'
|
||||||
|
| 'char';
|
||||||
|
keyCode: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NativefierOptions = Partial<
|
export type NativefierOptions = Partial<
|
||||||
@ -81,9 +102,20 @@ export type NativefierOptions = Partial<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export type OutputOptions = NativefierOptions & {
|
export type OutputOptions = NativefierOptions & {
|
||||||
|
blockExternalUrls: boolean;
|
||||||
|
browserwindowOptions?: BrowserWindowOptions;
|
||||||
buildDate: number;
|
buildDate: number;
|
||||||
|
companyName?: string;
|
||||||
|
disableDevTools: boolean;
|
||||||
|
fileDownloadOptions?: Record<string, unknown>;
|
||||||
|
internalUrls: string | RegExp | undefined;
|
||||||
isUpgrade: boolean;
|
isUpgrade: boolean;
|
||||||
|
name: string;
|
||||||
|
nativefierVersion: string;
|
||||||
oldBuildWarningText: string;
|
oldBuildWarningText: string;
|
||||||
|
targetUrl: string;
|
||||||
|
userAgent?: string;
|
||||||
|
zoom?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PackageJSON = {
|
export type PackageJSON = {
|
||||||
@ -120,7 +152,7 @@ export type RawOptions = {
|
|||||||
electronVersionUsed?: string;
|
electronVersionUsed?: string;
|
||||||
enableEs3Apis?: boolean;
|
enableEs3Apis?: boolean;
|
||||||
fastQuit?: boolean;
|
fastQuit?: boolean;
|
||||||
fileDownloadOptions?: unknown;
|
fileDownloadOptions?: Record<string, unknown>;
|
||||||
flashPath?: string;
|
flashPath?: string;
|
||||||
flashPluginDir?: string;
|
flashPluginDir?: string;
|
||||||
fullScreen?: boolean;
|
fullScreen?: boolean;
|
||||||
@ -150,7 +182,7 @@ export type RawOptions = {
|
|||||||
showMenuBar?: boolean;
|
showMenuBar?: boolean;
|
||||||
singleInstance?: boolean;
|
singleInstance?: boolean;
|
||||||
targetUrl?: string;
|
targetUrl?: string;
|
||||||
titleBarStyle?: string;
|
titleBarStyle?: TitleBarValue;
|
||||||
tray: TrayValue;
|
tray: TrayValue;
|
||||||
upgrade?: string | boolean;
|
upgrade?: string | boolean;
|
||||||
upgradeFrom?: string;
|
upgradeFrom?: string;
|
||||||
@ -165,3 +197,25 @@ export type RawOptions = {
|
|||||||
y?: number;
|
y?: number;
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WindowOptions = {
|
||||||
|
blockExternalUrls: boolean;
|
||||||
|
browserwindowOptions?: BrowserWindowOptions;
|
||||||
|
insecure: boolean;
|
||||||
|
internalUrls?: string | RegExp;
|
||||||
|
name: string;
|
||||||
|
proxyRules?: string;
|
||||||
|
targetUrl: string;
|
||||||
|
userAgent?: string;
|
||||||
|
zoom: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function outputOptionsToWindowOptions(
|
||||||
|
options: OutputOptions,
|
||||||
|
): WindowOptions {
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
insecure: options.insecure ?? false,
|
||||||
|
zoom: options.zoom ?? 1.0,
|
||||||
|
};
|
||||||
|
}
|
18
shared/tsconfig.json
Normal file
18
shared/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig-base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "./lib",
|
||||||
|
// Here we want to set target and lib to the *worst* of app/tsconfig.json and src/tsconfig.json
|
||||||
|
// (plus "dom"), because shared code will run both in CLI Node and app Node.
|
||||||
|
// See comments in app/tsconfig.json and src/tsconfig.json
|
||||||
|
"target": "es2018",
|
||||||
|
"lib": [
|
||||||
|
"es2018",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/**/*"
|
||||||
|
],
|
||||||
|
}
|
13
src/.eslintrc.js
Normal file
13
src/.eslintrc.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const base = require('../base-eslintrc');
|
||||||
|
|
||||||
|
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
|
||||||
|
module.exports = {
|
||||||
|
parser: base.parser,
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
plugins: base.plugins,
|
||||||
|
extends: base.extends,
|
||||||
|
rules: base.rules,
|
||||||
|
};
|
@ -9,7 +9,7 @@ import {
|
|||||||
convertToIcns,
|
convertToIcns,
|
||||||
convertToTrayIcon,
|
convertToTrayIcon,
|
||||||
} from '../helpers/iconShellHelpers';
|
} from '../helpers/iconShellHelpers';
|
||||||
import { AppOptions } from '../options/model';
|
import { AppOptions } from '../../shared/src/options/model';
|
||||||
|
|
||||||
function iconIsIco(iconPath: string): boolean {
|
function iconIsIco(iconPath: string): boolean {
|
||||||
return path.extname(iconPath) === '.ico';
|
return path.extname(iconPath) === '.ico';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import * as electronGet from '@electron/get';
|
import * as electronGet from '@electron/get';
|
||||||
import * as electronPackager from 'electron-packager';
|
import electronPackager from 'electron-packager';
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import { convertIconIfNecessary } from './buildIcon';
|
import { convertIconIfNecessary } from './buildIcon';
|
||||||
@ -13,7 +13,7 @@ import {
|
|||||||
isWindowsAdmin,
|
isWindowsAdmin,
|
||||||
} from '../helpers/helpers';
|
} from '../helpers/helpers';
|
||||||
import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade';
|
import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade';
|
||||||
import { AppOptions, RawOptions } from '../options/model';
|
import { AppOptions, RawOptions } from '../../shared/src/options/model';
|
||||||
import { getOptions } from '../options/optionsMain';
|
import { getOptions } from '../options/optionsMain';
|
||||||
import { prepareElectronApp } from './prepareElectronApp';
|
import { prepareElectronApp } from './prepareElectronApp';
|
||||||
|
|
||||||
|
@ -6,8 +6,13 @@ import { promisify } from 'util';
|
|||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import { copyFileOrDir, generateRandomSuffix } from '../helpers/helpers';
|
import { copyFileOrDir, generateRandomSuffix } from '../helpers/helpers';
|
||||||
import { AppOptions, OutputOptions, PackageJSON } from '../options/model';
|
import {
|
||||||
|
AppOptions,
|
||||||
|
OutputOptions,
|
||||||
|
PackageJSON,
|
||||||
|
} from '../../shared/src/options/model';
|
||||||
import { parseJson } from '../utils/parseUtils';
|
import { parseJson } from '../utils/parseUtils';
|
||||||
|
import { DEFAULT_APP_NAME } from '../constants';
|
||||||
|
|
||||||
const writeFileAsync = promisify(fs.writeFile);
|
const writeFileAsync = promisify(fs.writeFile);
|
||||||
|
|
||||||
@ -66,7 +71,7 @@ function pickElectronAppArgs(options: AppOptions): OutputOptions {
|
|||||||
maxWidth: options.nativefier.maxWidth,
|
maxWidth: options.nativefier.maxWidth,
|
||||||
minHeight: options.nativefier.minHeight,
|
minHeight: options.nativefier.minHeight,
|
||||||
minWidth: options.nativefier.minWidth,
|
minWidth: options.nativefier.minWidth,
|
||||||
name: options.packager.name,
|
name: options.packager.name ?? DEFAULT_APP_NAME,
|
||||||
nativefierVersion: options.nativefier.nativefierVersion,
|
nativefierVersion: options.nativefier.nativefierVersion,
|
||||||
osxNotarize: options.packager.osxNotarize,
|
osxNotarize: options.packager.osxNotarize,
|
||||||
osxSign: options.packager.osxSign,
|
osxSign: options.packager.osxSign,
|
||||||
|
@ -3,7 +3,7 @@ import 'source-map-support/register';
|
|||||||
|
|
||||||
import electronPackager = require('electron-packager');
|
import electronPackager = require('electron-packager');
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
import * as yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
|
|
||||||
import { DEFAULT_ELECTRON_VERSION } from './constants';
|
import { DEFAULT_ELECTRON_VERSION } from './constants';
|
||||||
import {
|
import {
|
||||||
@ -13,7 +13,7 @@ import {
|
|||||||
} from './helpers/helpers';
|
} from './helpers/helpers';
|
||||||
import { supportedArchs, supportedPlatforms } from './infer/inferOs';
|
import { supportedArchs, supportedPlatforms } from './infer/inferOs';
|
||||||
import { buildNativefierApp } from './main';
|
import { buildNativefierApp } from './main';
|
||||||
import { RawOptions } from './options/model';
|
import { RawOptions } from '../shared/src/options/model';
|
||||||
import { parseJson } from './utils/parseUtils';
|
import { parseJson } from './utils/parseUtils';
|
||||||
|
|
||||||
export function initArgs(argv: string[]): yargs.Argv<RawOptions> {
|
export function initArgs(argv: string[]): yargs.Argv<RawOptions> {
|
||||||
|
@ -3,7 +3,7 @@ import * as path from 'path';
|
|||||||
|
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import { NativefierOptions } from '../../options/model';
|
import { NativefierOptions } from '../../../shared/src/options/model';
|
||||||
import { getVersionString } from './rceditGet';
|
import { getVersionString } from './rceditGet';
|
||||||
import { fileExists } from '../fsHelpers';
|
import { fileExists } from '../fsHelpers';
|
||||||
type ExecutableInfo = {
|
type ExecutableInfo = {
|
||||||
|
@ -3,7 +3,10 @@ import * as path from 'path';
|
|||||||
|
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import { NativefierOptions, RawOptions } from '../../options/model';
|
import {
|
||||||
|
NativefierOptions,
|
||||||
|
RawOptions,
|
||||||
|
} from '../../../shared/src/options/model';
|
||||||
import { dirExists, fileExists } from '../fsHelpers';
|
import { dirExists, fileExists } from '../fsHelpers';
|
||||||
import { extractBoolean, extractString } from './plistInfoXMLHelpers';
|
import { extractBoolean, extractString } from './plistInfoXMLHelpers';
|
||||||
import { getOptionsFromExecutable } from './executableHelpers';
|
import { getOptionsFromExecutable } from './executableHelpers';
|
||||||
|
@ -3,7 +3,7 @@ import { writeFile } from 'fs';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
import gitCloud = require('gitcloud');
|
import gitCloud = require('gitcloud');
|
||||||
import * as pageIcon from 'page-icon';
|
import pageIcon from 'page-icon';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
downloadFile,
|
||||||
|
@ -10,7 +10,7 @@ import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion';
|
|||||||
import { inferArch } from './infer/inferOs';
|
import { inferArch } from './infer/inferOs';
|
||||||
import { buildNativefierApp } from './main';
|
import { buildNativefierApp } from './main';
|
||||||
import { userAgent } from './options/fields/userAgent';
|
import { userAgent } from './options/fields/userAgent';
|
||||||
import { NativefierOptions, RawOptions } from './options/model';
|
import { NativefierOptions, RawOptions } from '../shared/src/options/model';
|
||||||
import { parseJson } from './utils/parseUtils';
|
import { parseJson } from './utils/parseUtils';
|
||||||
|
|
||||||
async function checkApp(
|
async function checkApp(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'source-map-support/register';
|
import 'source-map-support/register';
|
||||||
|
|
||||||
import { buildNativefierApp } from './build/buildNativefierApp';
|
import { buildNativefierApp } from './build/buildNativefierApp';
|
||||||
import { RawOptions } from './options/model';
|
import { RawOptions } from '../shared/src/options/model';
|
||||||
|
|
||||||
export { buildNativefierApp };
|
export { buildNativefierApp };
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import { processOptions } from './fields/fields';
|
import { processOptions } from './fields/fields';
|
||||||
import { AppOptions } from './model';
|
import { AppOptions } from '../../shared/src/options/model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes the options object and infers new values needing async work
|
* Takes the options object and infers new values needing async work
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AppOptions } from '../model';
|
import { AppOptions } from '../../../shared/src/options/model';
|
||||||
import { processOptions } from './fields';
|
import { processOptions } from './fields';
|
||||||
describe('fields', () => {
|
describe('fields', () => {
|
||||||
let options: AppOptions;
|
let options: AppOptions;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { icon } from './icon';
|
import { icon } from './icon';
|
||||||
import { userAgent } from './userAgent';
|
import { userAgent } from './userAgent';
|
||||||
import { AppOptions } from '../model';
|
import { AppOptions } from '../../../shared/src/options/model';
|
||||||
import { name } from './name';
|
import { name } from './name';
|
||||||
|
|
||||||
type OptionPostprocessor = {
|
type OptionPostprocessor = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { getOptions, normalizePlatform } from './optionsMain';
|
import { getOptions, normalizePlatform } from './optionsMain';
|
||||||
import * as asyncConfig from './asyncConfig';
|
import * as asyncConfig from './asyncConfig';
|
||||||
import { inferPlatform } from '../infer/inferOs';
|
import { inferPlatform } from '../infer/inferOs';
|
||||||
import { AppOptions, RawOptions } from './model';
|
import { AppOptions, RawOptions } from '../../shared/src/options/model';
|
||||||
|
|
||||||
let asyncConfigMock: jest.SpyInstance;
|
let asyncConfigMock: jest.SpyInstance;
|
||||||
const mockedAsyncConfig: AppOptions = {
|
const mockedAsyncConfig: AppOptions = {
|
||||||
|
@ -19,7 +19,11 @@ import {
|
|||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { inferPlatform, inferArch } from '../infer/inferOs';
|
import { inferPlatform, inferArch } from '../infer/inferOs';
|
||||||
import { asyncConfig } from './asyncConfig';
|
import { asyncConfig } from './asyncConfig';
|
||||||
import { AppOptions, GlobalShortcut, RawOptions } from './model';
|
import {
|
||||||
|
AppOptions,
|
||||||
|
GlobalShortcut,
|
||||||
|
RawOptions,
|
||||||
|
} from '../../shared/src/options/model';
|
||||||
import { normalizeUrl } from './normalizeUrl';
|
import { normalizeUrl } from './normalizeUrl';
|
||||||
import { parseJson } from '../utils/parseUtils';
|
import { parseJson } from '../utils/parseUtils';
|
||||||
|
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../tsconfig-base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": false,
|
"outDir": "../lib",
|
||||||
"declaration": true,
|
"rootDir": ".",
|
||||||
"incremental": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"outDir": "./lib",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
// Bumping the minimum required Node version? You must bump:
|
// Bumping the minimum required Node version? You must bump:
|
||||||
// 1. package.json -> engines.node
|
// 1. package.json -> engines.node
|
||||||
// 2. package.json -> devDependencies.@types/node
|
// 2. package.json -> devDependencies.@types/node
|
||||||
@ -28,9 +21,11 @@
|
|||||||
"lib": [
|
"lib": [
|
||||||
"es2020",
|
"es2020",
|
||||||
"dom"
|
"dom"
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
"include": [
|
"references": [
|
||||||
"./src/**/*"
|
{
|
||||||
|
"path": "../shared"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
14
tsconfig-base.json
Normal file
14
tsconfig-base.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": false,
|
||||||
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"incremental": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
},
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user