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:
Adam Weeden 2021-06-26 09:59:28 -04:00 committed by GitHub
parent f8f48d2f09
commit b74c0bf959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 653 additions and 385 deletions

2
.github/manual-test vendored
View File

@ -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

View File

@ -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'
},
}; };

View File

@ -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",

View File

@ -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,

View File

@ -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();

View File

@ -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! ;)

View File

@ -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);

View File

@ -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);

View File

@ -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;
} }

View File

@ -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') {

View File

@ -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;
} }

View File

@ -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();

View File

@ -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();

View File

@ -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',
{ {

View File

@ -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;

View File

@ -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 });
}); });

View File

@ -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 {

View File

@ -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 });
}); });

View File

@ -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"
}
] ]
} }

View File

@ -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',

View File

@ -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
View 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/**'],
};

View File

@ -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
View 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
View 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,
};

View File

@ -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';

View File

@ -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';

View File

@ -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,

View File

@ -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> {

View File

@ -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 = {

View File

@ -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';

View File

@ -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,

View File

@ -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(

View File

@ -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 };

View File

@ -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

View File

@ -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;

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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';

View File

@ -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
View 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,
},
}