2
2
mirror of https://github.com/Llewellynvdm/nativefier.git synced 2024-12-22 10:08:55 +00:00

Update to Electron 25 (#1559)

This is intended to get Electron updated to 25. There are no known bugs
in this release.

As well this includes a fix for an existing bug I noticed where child
windows in Windows received a menu bar when they should not have.
This commit is contained in:
Adam Weeden 2023-08-25 09:10:05 -04:00 committed by GitHub
parent be418d4349
commit 64157c3c5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2240 additions and 8170 deletions

View File

@ -8,7 +8,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v1 # Setup .npmrc file to publish to npm - uses: actions/setup-node@v2 # Setup .npmrc file to publish to npm
with: with:
node-version: '20' # Align the version of Node here with ci.yml. node-version: '20' # Align the version of Node here with ci.yml.
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v1 # Setup .npmrc file to publish to npm - uses: actions/setup-node@v2 # Setup .npmrc file to publish to npm
with: with:
node-version: '20' # Align the version of Node here with ci.yml. node-version: '20' # Align the version of Node here with ci.yml.
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

1
.gitignore vendored
View File

@ -44,7 +44,6 @@ build/Release
# Dependency directory # Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules node_modules
.nvmrc
# Python virtual environment in case it's created for the Castlabs code signing tool # Python virtual environment in case it's created for the Castlabs code signing tool
venv venv

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
16

1333
app/npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,14 +12,14 @@
], ],
"scripts": {}, "scripts": {},
"dependencies": { "dependencies": {
"electron-context-menu": "^3.1.1", "electron-context-menu": "^3.6.1",
"electron-dl": "^3.2.1", "electron-dl": "^3.5.0",
"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.8.1",
"source-map-support": "^0.5.19" "source-map-support": "^0.5.21"
}, },
"devDependencies": { "devDependencies": {
"electron": "^21.4.4" "electron": "^25.5.0"
} }
} }

View File

@ -1,7 +1,7 @@
import { import {
BrowserWindow, BrowserWindow,
ContextMenuParams, ContextMenuParams,
NewWindowWebContentsEvent, Event as ElectronEvent,
} from 'electron'; } from 'electron';
import contextMenu from 'electron-context-menu'; import contextMenu from 'electron-context-menu';
@ -25,7 +25,7 @@ export function initContextMenu(
prepend: (actions: contextMenu.Actions, params: ContextMenuParams) => { 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 && window) {
items.push({ items.push({
label: 'Open Link in Default Browser', label: 'Open Link in Default Browser',
click: () => { click: () => {
@ -38,36 +38,40 @@ export function initContextMenu(
label: 'Open Link in New Window', label: 'Open Link in New Window',
click: () => click: () =>
createNewWindow( createNewWindow(
outputOptionsToWindowOptions(options), outputOptionsToWindowOptions(options, nativeTabsSupported()),
setupNativefierWindow, setupNativefierWindow,
params.linkURL, params.linkURL,
window, // window,
), ),
}); });
if (nativeTabsSupported()) { if (nativeTabsSupported()) {
items.push({ items.push({
label: 'Open Link in New Tab', label: 'Open Link in New Tab',
click: () => click: () =>
// Fire a new window event for a foreground tab // // Fire a new window event for a foreground tab
// Previously we called createNewTab directly, but it had incosistent and buggy behavior // // Previously we called createNewTab directly, but it had incosistent and buggy behavior
// as it was mostly designed for running off of events. So this will create a new event // // as it was mostly designed for running off of events. So this will create a new event
// for a foreground-tab for the event handler to grab and take care of instead. // // for a foreground-tab for the event handler to grab and take care of instead.
(window as BrowserWindow).webContents.emit( // (window as BrowserWindow).webContents.emit(
// event name // // event name
'new-window', // 'new-window',
// event object // // event object
{ // {
// Leave to the default for a NewWindowWebContentsEvent // // Leave to the default for a NewWindowWebContentsEvent
newGuest: undefined, // newGuest: undefined,
...new Event('new-window'), // ...new Event('new-window'),
} as NewWindowWebContentsEvent, // }, // as NewWindowWebContentsEvent,
// url // // url
params.linkURL, // params.linkURL,
// frameName // // frameName
window?.webContents.mainFrame.name ?? '', // window?.webContents.mainFrame.name ?? '',
// disposition // // disposition
'foreground-tab', // 'foreground-tab',
), // ),
window.emit('new-window-for-tab', {
...new Event('new-window-for-tab'),
url: params.linkURL,
} as ElectronEvent<{ url: string }>),
}); });
} }
} }

View File

@ -3,15 +3,19 @@ import * as path from 'path';
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
import * as log from '../helpers/loggingHelper'; import * as log from '../helpers/loggingHelper';
import { nativeTabsSupported } from '../helpers/helpers';
export async function createLoginWindow( export async function createLoginWindow(
loginCallback: (username?: string, password?: string) => void, loginCallback: (username?: string, password?: string) => void,
parent?: BrowserWindow, parent?: BrowserWindow,
): Promise<BrowserWindow> { ): Promise<BrowserWindow> {
log.debug('createLoginWindow', { loginCallback, parent }); log.debug('createLoginWindow', {
loginCallback,
parent,
});
const loginWindow = new BrowserWindow({ const loginWindow = new BrowserWindow({
parent, parent: nativeTabsSupported() ? undefined : parent,
width: 300, width: 300,
height: 400, height: 400,
frame: false, frame: false,

View File

@ -1,7 +1,13 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { ipcMain, desktopCapturer, BrowserWindow, Event } from 'electron'; import {
desktopCapturer,
ipcMain,
BrowserWindow,
Event,
HandlerDetails,
} from 'electron';
import windowStateKeeper from 'electron-window-state'; import windowStateKeeper from 'electron-window-state';
import { initContextMenu } from './contextMenu'; import { initContextMenu } from './contextMenu';
@ -36,9 +42,9 @@ type SessionInteractionRequest = {
propertyValue?: unknown; propertyValue?: unknown;
}; };
type SessionInteractionResult = { type SessionInteractionResult<T = unknown> = {
id?: string; id?: string;
value?: unknown | Promise<unknown>; value?: T | Promise<T>;
error?: Error; error?: Error;
}; };
@ -78,7 +84,9 @@ export async function createMainWindow(
// So, we manually mainWindow.show() later, see a few lines below // So, we manually mainWindow.show() later, see a few lines below
show: options.tray !== 'start-in-tray' && process.platform !== 'win32', show: options.tray !== 'start-in-tray' && process.platform !== 'win32',
backgroundColor: options.backgroundColor, backgroundColor: options.backgroundColor,
...getDefaultWindowOptions(outputOptionsToWindowOptions(options)), ...getDefaultWindowOptions(
outputOptionsToWindowOptions(options, nativeTabsSupported()),
),
}); });
// Just load about:blank to start, gives playwright something to latch onto initially for testing. // Just load about:blank to start, gives playwright something to latch onto initially for testing.
@ -102,44 +110,32 @@ export async function createMainWindow(
mainWindow.show(); mainWindow.show();
} }
const windowOptions = outputOptionsToWindowOptions(options); const windowOptions = outputOptionsToWindowOptions(
options,
nativeTabsSupported(),
);
createMenu(options, mainWindow); createMenu(options, mainWindow);
createContextMenu(options, mainWindow); createContextMenu(options, mainWindow);
setupNativefierWindow(windowOptions, mainWindow); setupNativefierWindow(windowOptions, mainWindow);
// .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...)
// We can't quite cut over to that yet for a few reasons:
// 1. Our version of Electron does not yet support a parameter to
// setWindowOpenHandler that contains `disposition', which we need.
// See https://github.com/electron/electron/issues/28380
// 2. setWindowOpenHandler doesn't support newGuest as well
// Though at this point, 'new-window' bugs seem to be coming up and downstream
// users are being pointed to use setWindowOpenHandler.
// E.g., https://github.com/electron/electron/issues/28374
// Note it is important to add these handlers only to the *main* window, // Note it is important to add these handlers only to the *main* window,
// else we run into weird behavior like opening tabs twice // else we run into weird behavior like opening tabs twice
mainWindow.webContents.on( mainWindow.webContents.setWindowOpenHandler((details: HandlerDetails) => {
'new-window', return onNewWindow(
(event, url, frameName, disposition) => { windowOptions,
onNewWindow( setupNativefierWindow,
windowOptions, details,
setupNativefierWindow, mainWindow,
event, );
url, });
frameName, mainWindow.on('new-window-for-tab', (event?: Event<{ url?: string }>) => {
disposition, log.debug('mainWindow.new-window-for-tab', { event });
).catch((err) => log.error('onNewWindow ERROR', err));
},
);
// @ts-expect-error new-tab isn't in the type definition, but it does exist
mainWindow.on('new-tab', () => {
createNewTab( createNewTab(
windowOptions, windowOptions,
setupNativefierWindow, setupNativefierWindow,
options.targetUrl, event?.url ?? options.targetUrl,
true, true,
mainWindow, // mainWindow,
); );
}); });
@ -154,7 +150,7 @@ export async function createMainWindow(
mainWindow.show(); mainWindow.show();
}); });
setupSessionInteraction(options, mainWindow); setupSessionInteraction(mainWindow);
setupSessionPermissionHandler(mainWindow); setupSessionPermissionHandler(mainWindow);
if (options.clearCache) { if (options.clearCache) {
@ -265,10 +261,7 @@ function setupNotificationBadge(
}); });
} }
function setupSessionInteraction( function setupSessionInteraction(window: BrowserWindow): void {
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',

View File

@ -2,7 +2,7 @@ jest.mock('./helpers');
jest.mock('./windowEvents'); jest.mock('./windowEvents');
jest.mock('./windowHelpers'); jest.mock('./windowHelpers');
import { dialog, BrowserWindow } from 'electron'; import { dialog, BrowserWindow, HandlerDetails, WebContents } from 'electron';
import { WindowOptions } from '../../../shared/src/options/model'; import { WindowOptions } from '../../../shared/src/options/model';
import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers'; import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@ -14,11 +14,9 @@ const {
onNewWindowHelper: ( onNewWindowHelper: (
options: WindowOptions, options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void, setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
urlToGo: string, details: Partial<HandlerDetails>,
disposition: string | undefined,
preventDefault: (newGuest: BrowserWindow) => void,
parent?: BrowserWindow, parent?: BrowserWindow,
) => Promise<void>; ) => ReturnType<Parameters<WebContents['setWindowOpenHandler']>[0]>;
onWillNavigate: ( onWillNavigate: (
options: { options: {
blockExternalUrls: boolean; blockExternalUrls: boolean;
@ -43,12 +41,13 @@ describe('onNewWindowHelper', () => {
const foregroundDisposition = 'foreground-tab'; const foregroundDisposition = 'foreground-tab';
const backgroundDisposition = 'background-tab'; const backgroundDisposition = 'background-tab';
const baseOptions = { const baseOptions = {
autoHideMenuBar: true,
blockExternalUrls: false, blockExternalUrls: false,
insecure: false, insecure: false,
name: 'TEST_APP', name: 'TEST_APP',
targetUrl: originalURL, targetUrl: originalURL,
zoom: 1.0, zoom: 1.0,
}; } as WindowOptions;
const mockShowNavigationBlockedMessage: jest.SpyInstance = const mockShowNavigationBlockedMessage: jest.SpyInstance =
showNavigationBlockedMessage as jest.Mock; showNavigationBlockedMessage as jest.Mock;
const mockCreateAboutBlank: jest.SpyInstance = const mockCreateAboutBlank: jest.SpyInstance =
@ -60,7 +59,6 @@ describe('onNewWindowHelper', () => {
const mockNativeTabsSupported: jest.SpyInstance = const mockNativeTabsSupported: jest.SpyInstance =
nativeTabsSupported as jest.Mock; nativeTabsSupported as jest.Mock;
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock; const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
const preventDefault = jest.fn();
const setupWindow = jest.fn(); const setupWindow = jest.fn();
beforeEach(() => { beforeEach(() => {
@ -72,7 +70,6 @@ describe('onNewWindowHelper', () => {
mockLinkIsInternal.mockReset().mockReturnValue(true); mockLinkIsInternal.mockReset().mockReturnValue(true);
mockNativeTabsSupported.mockReset().mockReturnValue(false); mockNativeTabsSupported.mockReset().mockReturnValue(false);
mockOpenExternal.mockReset(); mockOpenExternal.mockReset();
preventDefault.mockReset();
setupWindow.mockReset(); setupWindow.mockReset();
}); });
@ -85,105 +82,84 @@ describe('onNewWindowHelper', () => {
mockOpenExternal.mockRestore(); mockOpenExternal.mockRestore();
}); });
test('internal urls should not be handled', async () => { test('internal urls should not be handled', () => {
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: internalURL,
setupWindow, });
internalURL,
undefined,
preventDefault,
);
expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled(); expect(result.action).toEqual('allow');
}); });
test('external urls should be opened externally', async () => { test('external urls should be opened externally', () => {
mockLinkIsInternal.mockReturnValue(false); mockLinkIsInternal.mockReturnValue(false);
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: externalURL,
setupWindow, });
externalURL,
undefined,
preventDefault,
);
expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1); expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(preventDefault).toHaveBeenCalledTimes(1); expect(result.action).toEqual('deny');
}); });
test('external urls should be ignored if blockExternalUrls is true', async () => { test('external urls should be ignored if blockExternalUrls is true', () => {
mockLinkIsInternal.mockReturnValue(false); mockLinkIsInternal.mockReturnValue(false);
const options = { const options = {
...baseOptions, ...baseOptions,
blockExternalUrls: true, blockExternalUrls: true,
}; };
await onNewWindowHelper( const result = onNewWindowHelper(options, setupWindow, {
options, url: externalURL,
setupWindow, });
externalURL,
undefined,
preventDefault,
);
expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1); expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockOpenExternal).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledTimes(1); expect(result.action).toEqual('deny');
}); });
test('tab disposition should be ignored if tabs are not enabled', async () => { test('tab disposition should be ignored if tabs are not enabled', () => {
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: internalURL,
setupWindow, disposition: foregroundDisposition,
internalURL, });
foregroundDisposition,
preventDefault,
);
expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled(); expect(result.action).toEqual('allow');
}); });
test('tab disposition should be ignored if url is external', async () => { test('tab disposition should be ignored if url is external', () => {
mockLinkIsInternal.mockReturnValue(false); mockLinkIsInternal.mockReturnValue(false);
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: externalURL,
setupWindow, disposition: foregroundDisposition,
externalURL, });
foregroundDisposition,
preventDefault,
);
expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1); expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(preventDefault).toHaveBeenCalledTimes(1); expect(result.action).toEqual('deny');
}); });
test('foreground tabs with internal urls should be opened in the foreground', async () => { test('foreground tabs with internal urls should be opened in the foreground', () => {
mockNativeTabsSupported.mockReturnValue(true); mockNativeTabsSupported.mockReturnValue(true);
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: internalURL,
setupWindow, disposition: foregroundDisposition,
internalURL, });
foregroundDisposition,
preventDefault,
);
expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).toHaveBeenCalledTimes(1); expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
@ -192,23 +168,19 @@ describe('onNewWindowHelper', () => {
setupWindow, setupWindow,
internalURL, internalURL,
true, true,
undefined,
); );
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledTimes(1); expect(result.action).toEqual('deny');
}); });
test('background tabs with internal urls should be opened in background tabs', async () => { test('background tabs with internal urls should be opened in background tabs', () => {
mockNativeTabsSupported.mockReturnValue(true); mockNativeTabsSupported.mockReturnValue(true);
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: internalURL,
setupWindow, disposition: backgroundDisposition,
internalURL, });
backgroundDisposition,
preventDefault,
);
expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).toHaveBeenCalledTimes(1); expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
@ -217,59 +189,46 @@ describe('onNewWindowHelper', () => {
setupWindow, setupWindow,
internalURL, internalURL,
false, false,
undefined,
); );
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledTimes(1); expect(result.action).toEqual('deny');
}); });
test('about:blank urls should be handled', async () => { test('about:blank urls should be handled', () => {
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: 'about:blank',
setupWindow, });
'about:blank',
undefined,
preventDefault,
);
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1); expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledTimes(1); expect(result.action).toEqual('deny');
}); });
test('about:blank#blocked urls should be handled', async () => { test('about:blank#blocked urls should be handled', () => {
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: 'about:blank#blocked',
setupWindow, });
'about:blank#blocked',
undefined,
preventDefault,
);
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1); expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledTimes(1); expect(result.action).toEqual('deny');
}); });
test('about:blank#other urls should not be handled', async () => { test('about:blank#other urls should not be handled', () => {
await onNewWindowHelper( const result = onNewWindowHelper(baseOptions, setupWindow, {
baseOptions, url: 'about:blank#other',
setupWindow, });
'about:blank#other',
undefined,
preventDefault,
);
expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled(); expect(result.action).toEqual('allow');
}); });
}); });

View File

@ -2,8 +2,8 @@ import {
dialog, dialog,
BrowserWindow, BrowserWindow,
Event, Event,
NewWindowWebContentsEvent,
WebContents, WebContents,
HandlerDetails,
} from 'electron'; } from 'electron';
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers'; import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
@ -18,84 +18,64 @@ import {
} from './windowHelpers'; } from './windowHelpers';
import { WindowOptions } from '../../../shared/src/options/model'; import { WindowOptions } from '../../../shared/src/options/model';
type NewWindowHandlerResult = ReturnType<
Parameters<WebContents['setWindowOpenHandler']>[0]
>;
export function onNewWindow( export function onNewWindow(
options: WindowOptions, options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void, setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
event: NewWindowWebContentsEvent, details: HandlerDetails,
urlToGo: string,
frameName: string,
disposition:
| 'default'
| 'foreground-tab'
| 'background-tab'
| 'new-window'
| 'save-to-disk'
| 'other',
parent?: BrowserWindow, parent?: BrowserWindow,
): Promise<void> { ): NewWindowHandlerResult {
log.debug('onNewWindow', { log.debug('onNewWindow', {
event, details,
urlToGo,
frameName,
disposition,
parent,
}); });
const preventDefault = (newGuest?: BrowserWindow): void => {
log.debug('onNewWindow.preventDefault', { newGuest, event });
if (event.preventDefault) {
event.preventDefault();
}
if (newGuest) {
event.newGuest = newGuest;
}
};
return onNewWindowHelper( return onNewWindowHelper(
options, options,
setupWindow, setupWindow,
urlToGo, details,
disposition, nativeTabsSupported() ? undefined : parent,
preventDefault,
parent,
); );
} }
export function onNewWindowHelper( export function onNewWindowHelper(
options: WindowOptions, options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void, setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
urlToGo: string, details: HandlerDetails,
disposition: string | undefined,
preventDefault: (newGuest?: BrowserWindow) => void,
parent?: BrowserWindow, parent?: BrowserWindow,
): Promise<void> { ): NewWindowHandlerResult {
log.debug('onNewWindowHelper', { log.debug('onNewWindowHelper', {
options, options,
urlToGo, details,
disposition,
preventDefault,
parent,
}); });
try { try {
if ( if (
!linkIsInternal( !linkIsInternal(
options.targetUrl, options.targetUrl,
urlToGo, details.url,
options.internalUrls, options.internalUrls,
options.strictInternalUrls, options.strictInternalUrls,
) )
) { ) {
preventDefault();
if (options.blockExternalUrls) { if (options.blockExternalUrls) {
return new Promise((resolve) => { showNavigationBlockedMessage(
showNavigationBlockedMessage( `Navigation to external URL blocked by options: ${details.url}`,
`Navigation to external URL blocked by options: ${urlToGo}`, )
) .then(() => {
.then(() => resolve()) // blockExternalURL(details.url).then(resolve).catch((err: unknown) => {
.catch((err: unknown) => { // log.error('blockExternalURL', err);
throw err; // });
}); })
}); .catch((err: unknown) => {
throw err;
});
return { action: 'deny' };
} else { } else {
return openExternal(urlToGo); openExternal(details.url).catch((err: unknown) => {
log.error('openExternal', err);
});
return { action: 'deny' };
} }
} }
// Normally the following would be: // Normally the following would be:
@ -103,26 +83,25 @@ export function onNewWindowHelper(
// But due to a bug we resolved in https://github.com/nativefier/nativefier/issues/1197 // But due to a bug we resolved in https://github.com/nativefier/nativefier/issues/1197
// Some sites use about:blank#something to use as placeholder windows to fill // Some sites use about:blank#something to use as placeholder windows to fill
// with content via JavaScript. So we'll stay specific for now... // with content via JavaScript. So we'll stay specific for now...
else if (['about:blank', 'about:blank#blocked'].includes(urlToGo)) { else if (['about:blank', 'about:blank#blocked'].includes(details.url)) {
return Promise.resolve( createAboutBlankWindow(
preventDefault(createAboutBlankWindow(options, setupWindow, parent)), options,
setupWindow,
nativeTabsSupported() ? undefined : parent,
); );
return { action: 'deny' };
} else if (nativeTabsSupported()) { } else if (nativeTabsSupported()) {
return Promise.resolve( createNewTab(
preventDefault( options,
createNewTab( setupWindow,
options, details.url,
setupWindow, details.disposition === 'foreground-tab',
urlToGo,
disposition === 'foreground-tab',
parent,
),
),
); );
return { action: 'deny' };
} }
return Promise.resolve(undefined); return { action: 'allow' };
} catch (err: unknown) { } catch (err: unknown) {
return Promise.reject(err); return { action: 'deny' };
} }
} }

View File

@ -52,14 +52,15 @@ describe('clearAppData', () => {
}); });
describe('createNewTab', () => { describe('createNewTab', () => {
const window = new BrowserWindow(); // const window = new BrowserWindow();
const options: WindowOptions = { const options: WindowOptions = {
autoHideMenuBar: true,
blockExternalUrls: false, blockExternalUrls: false,
insecure: false, insecure: false,
name: 'Test App', name: 'Test App',
targetUrl: 'https://github.com/nativefier/natifefier', targetUrl: 'https://github.com/nativefier/natifefier',
zoom: 1.0, zoom: 1.0,
}; } as WindowOptions;
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(
@ -78,7 +79,7 @@ describe('createNewTab', () => {
test('creates new foreground tab', () => { test('creates new foreground tab', () => {
const foreground = true; const foreground = true;
const tab = createNewTab(options, setupWindow, url, foreground, window); const tab = createNewTab(options, setupWindow, url, foreground);
expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab); expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab);
expect(setupWindow).toHaveBeenCalledWith(options, tab); expect(setupWindow).toHaveBeenCalledWith(options, tab);
@ -89,7 +90,13 @@ describe('createNewTab', () => {
test('creates new background tab', () => { test('creates new background tab', () => {
const foreground = false; const foreground = false;
const tab = createNewTab(options, setupWindow, url, foreground, window); const tab = createNewTab(
options,
setupWindow,
url,
foreground,
// window
);
expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab); expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab);
expect(setupWindow).toHaveBeenCalledWith(options, tab); expect(setupWindow).toHaveBeenCalledWith(options, tab);

View File

@ -13,6 +13,7 @@ import {
import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers'; import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers';
import * as log from './loggingHelper'; import * as log from './loggingHelper';
import { TrayValue, WindowOptions } from '../../../shared/src/options/model'; import { TrayValue, WindowOptions } from '../../../shared/src/options/model';
import { randomUUID } from 'crypto';
const ZOOM_INTERVAL = 0.1; const ZOOM_INTERVAL = 0.1;
@ -73,7 +74,7 @@ export function createAboutBlankWindow(
{ ...options, show: false }, { ...options, show: false },
setupWindow, setupWindow,
'about:blank', 'about:blank',
parent, nativeTabsSupported() ? undefined : parent,
); );
window.webContents.once('did-stop-loading', () => { window.webContents.once('did-stop-loading', () => {
if (window.webContents.getURL() === 'about:blank') { if (window.webContents.getURL() === 'about:blank') {
@ -90,11 +91,16 @@ export function createNewTab(
setupWindow: (options: WindowOptions, window: BrowserWindow) => void, setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
url: string, url: string,
foreground: boolean, foreground: boolean,
parent?: BrowserWindow,
): BrowserWindow | undefined { ): BrowserWindow | undefined {
log.debug('createNewTab', { url, foreground, parent }); const focusedWindow = BrowserWindow.getFocusedWindow();
log.debug('createNewTab', {
url,
foreground,
focusedWindow,
});
return withFocusedWindow((focusedWindow) => { return withFocusedWindow((focusedWindow) => {
const newTab = createNewWindow(options, setupWindow, url, parent); const newTab = createNewWindow(options, setupWindow, url);
log.debug('createNewTab.withFocusedWindow', { focusedWindow, newTab });
focusedWindow.addTabbedWindow(newTab); focusedWindow.addTabbedWindow(newTab);
if (!foreground) { if (!foreground) {
focusedWindow.focus(); focusedWindow.focus();
@ -109,9 +115,12 @@ export function createNewWindow(
url: string, url: string,
parent?: BrowserWindow, parent?: BrowserWindow,
): BrowserWindow { ): BrowserWindow {
log.debug('createNewWindow', { url, parent }); log.debug('createNewWindow', {
const window = new BrowserWindow({ url,
parent, parent,
});
const window = new BrowserWindow({
parent: nativeTabsSupported() ? undefined : parent,
...getDefaultWindowOptions(options), ...getDefaultWindowOptions(options),
}); });
setupWindow(options, window); setupWindow(options, window);
@ -141,8 +150,11 @@ export function getDefaultWindowOptions(
}; };
const defaultOptions: BrowserWindowConstructorOptions = { const defaultOptions: BrowserWindowConstructorOptions = {
autoHideMenuBar: options.autoHideMenuBar,
fullscreenable: true, fullscreenable: true,
tabbingIdentifier: nativeTabsSupported() ? options.name : undefined, tabbingIdentifier: nativeTabsSupported()
? options.tabbingIdentifier ?? randomUUID()
: undefined,
title: options.name, title: options.name,
webPreferences: { webPreferences: {
javascript: true, javascript: true,

View File

@ -202,7 +202,8 @@ const setDockBadge = isOSX()
? (count?: number | string, bounce = false): void => { ? (count?: number | string, bounce = false): void => {
if (count !== undefined) { if (count !== undefined) {
app.dock.setBadge(count.toString()); app.dock.setBadge(count.toString());
if (bounce && count > currentBadgeCount) app.dock.bounce(); if (bounce && typeof count === 'number' && count > currentBadgeCount)
app.dock.bounce();
currentBadgeCount = typeof count === 'number' ? count : 0; currentBadgeCount = typeof count === 'number' ? count : 0;
} }
} }
@ -309,10 +310,10 @@ if (shouldQuit) {
}); });
} }
app.on('new-window-for-tab', () => { app.on('new-window-for-tab', (event: Event) => {
log.debug('app.new-window-for-tab'); log.debug('app.new-window-for-tab', { event });
if (mainWindow) { if (mainWindow) {
mainWindow.emit('new-tab'); mainWindow.emit('new-window-for-tab', event);
} }
}); });
@ -332,9 +333,10 @@ app.on(
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(
log.error('createLoginWindow ERROR', err), callback,
); // mainWindow
).catch((err) => log.error('createLoginWindow ERROR', err));
} }
}, },
); );

View File

@ -21,7 +21,6 @@ module.exports = {
'@typescript-eslint/no-confusing-non-null-assertion': 'error', '@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-extraneous-class': 'error', '@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-implicit-any-catch': 'error',
'@typescript-eslint/no-invalid-void-type': 'error', '@typescript-eslint/no-invalid-void-type': 'error',
'@typescript-eslint/prefer-ts-expect-error': 'error', '@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/type-annotation-spacing': 'error', '@typescript-eslint/type-annotation-spacing': 'error',

8531
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -58,43 +58,44 @@
"watch": "npx concurrently \"npm:*:watch\"" "watch": "npx concurrently \"npm:*:watch\""
}, },
"dependencies": { "dependencies": {
"axios": "^1.1.3", "@electron/asar": "^3.2.4",
"electron-packager": "^15.5.1", "axios": "^1.4.0",
"fs-extra": "^10.0.0", "electron-packager": "^17.1.1",
"gitcloud": "^0.2.3", "fs-extra": "^11.1.1",
"gitcloud": "^0.2.4",
"hasbin": "^1.2.3", "hasbin": "^1.2.3",
"loglevel": "^1.7.1", "loglevel": "^1.8.1",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"page-icon": "^0.4.0", "page-icon": "^0.4.0",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"source-map-support": "^0.5.19", "source-map-support": "^0.5.21",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"yargs": "^17.1.1" "yargs": "^17.7.2"
}, },
"devDependencies": { "devDependencies": {
"@types/debug": "^4.1.6", "@types/debug": "^4.1.8",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^11.0.1",
"@types/hasbin": "^1.2.0", "@types/hasbin": "^1.2.0",
"@types/jest": "^28.1.6", "@types/jest": "^29.5.3",
"@types/ncp": "^2.0.5", "@types/ncp": "^2.0.5",
"@types/node": "^16.0.0", "@types/node": "^20.4.7",
"@types/page-icon": "^0.3.4", "@types/page-icon": "^0.3.4",
"@types/tmp": "^0.2.1", "@types/tmp": "^0.2.3",
"@types/yargs": "^17.0.10", "@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^5.3.0", "@typescript-eslint/parser": "^6.2.1",
"electron": "^21.4.4", "electron": "^25.5.0",
"eslint": "^8.1.0", "eslint": "^8.46.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^5.0.0",
"jest": "^28.1.3", "jest": "^29.6.2",
"playwright": "^1.24.0", "playwright": "^1.36.2",
"prettier": "^2.3.2", "prettier": "^3.0.1",
"rimraf": "^3.0.2", "rimraf": "^5.0.1",
"ts-loader": "^9.2.3", "ts-loader": "^9.4.4",
"typescript": "^4.3.5", "typescript": "^5.1.6",
"webpack": "^5.45.1", "webpack": "^5.88.2",
"webpack-cli": "^4.7.2" "webpack-cli": "^5.1.4"
}, },
"jest_COMMENTS": { "jest_COMMENTS": {
"testPathIgnorePatterns": "See https://jestjs.io/docs/configuration#testpathignorepatterns-arraystring . We set it to 1. ignore coverage for deps, and 2. be sure we test the compiled JS, which is in `lib`, not `src` or `dist`", "testPathIgnorePatterns": "See https://jestjs.io/docs/configuration#testpathignorepatterns-arraystring . We set it to 1. ignore coverage for deps, and 2. be sure we test the compiled JS, which is in `lib`, not `src` or `dist`",

View File

@ -1,4 +1,5 @@
import { CreateOptions } from 'asar'; import { CreateOptions } from '@electron/asar';
import { randomUUID } from 'crypto';
import * as electronPackager from 'electron-packager'; import * as electronPackager from 'electron-packager';
export type TitleBarValue = export type TitleBarValue =
@ -117,6 +118,7 @@ export type OutputOptions = NativefierOptions & {
nativefierVersion: string; nativefierVersion: string;
oldBuildWarningText: string; oldBuildWarningText: string;
strictInternalUrls: boolean; strictInternalUrls: boolean;
tabbingIdentifier?: string;
targetUrl: string; targetUrl: string;
userAgent?: string; userAgent?: string;
zoom?: number; zoom?: number;
@ -205,6 +207,7 @@ export type RawOptions = {
}; };
export type WindowOptions = { export type WindowOptions = {
autoHideMenuBar: boolean;
blockExternalUrls: boolean; blockExternalUrls: boolean;
browserwindowOptions?: BrowserWindowOptions; browserwindowOptions?: BrowserWindowOptions;
insecure: boolean; insecure: boolean;
@ -213,6 +216,7 @@ export type WindowOptions = {
name: string; name: string;
proxyRules?: string; proxyRules?: string;
show?: boolean; show?: boolean;
tabbingIdentifier?: string;
targetUrl: string; targetUrl: string;
userAgent?: string; userAgent?: string;
zoom: number; zoom: number;
@ -220,10 +224,15 @@ export type WindowOptions = {
export function outputOptionsToWindowOptions( export function outputOptionsToWindowOptions(
options: OutputOptions, options: OutputOptions,
generateTabbingIdentifierIfMissing: boolean,
): WindowOptions { ): WindowOptions {
return { return {
...options, ...options,
autoHideMenuBar: !options.showMenuBar,
insecure: options.insecure ?? false, insecure: options.insecure ?? false,
tabbingIdentifier: generateTabbingIdentifierIfMissing
? options.tabbingIdentifier ?? randomUUID()
: options.tabbingIdentifier,
zoom: options.zoom ?? 1.0, zoom: options.zoom ?? 1.0,
}; };
} }

View File

@ -6,19 +6,19 @@ export const DEFAULT_APP_NAME = 'APP';
// - upgrade app / package.json / "devDependencies" / "electron" // - upgrade app / package.json / "devDependencies" / "electron"
// - upgrade package.json / "devDependencies" / "electron" // - upgrade package.json / "devDependencies" / "electron"
// Doing a *major* upgrade? Read https://github.com/nativefier/nativefier/blob/master/HACKING.md#deps-major-upgrading-electron // Doing a *major* upgrade? Read https://github.com/nativefier/nativefier/blob/master/HACKING.md#deps-major-upgrading-electron
export const DEFAULT_ELECTRON_VERSION = '21.4.4'; export const DEFAULT_ELECTRON_VERSION = '25.5.0';
// https://atom.io/download/atom-shell/index.json // https://atom.io/download/atom-shell/index.json
// https://www.electronjs.org/releases/stable // https://www.electronjs.org/releases/stable
export const DEFAULT_CHROME_VERSION = '106.0.5249.199'; export const DEFAULT_CHROME_VERSION = '114.0.5735.289';
// Update each of these periodically // Update each of these periodically
// https://product-details.mozilla.org/1.0/firefox_versions.json // https://product-details.mozilla.org/1.0/firefox_versions.json
export const DEFAULT_FIREFOX_VERSION = '116.0'; export const DEFAULT_FIREFOX_VERSION = '116.0.1';
// https://en.wikipedia.org/wiki/Safari_version_history // https://en.wikipedia.org/wiki/Safari_version_history
export const DEFAULT_SAFARI_VERSION = { export const DEFAULT_SAFARI_VERSION = {
majorVersion: 65, majorVersion: 16,
version: '16.5.2', version: '16.6',
webkitVersion: '605.1.15', webkitVersion: '605.1.15',
}; };

View File

@ -6,6 +6,7 @@
"incremental": true, "incremental": true,
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"noImplicitAny": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,