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