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

Make app strict TypeScript + linting (and add a shared project) (#1231)

* Convert app to strict typing + shared library

* Fix new code post-merge

* Remove extraneous lint ignores

* Apply suggestions from code review

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* Fix prettier complaint

* Dedupe eslint files

* Fix some refs after merge

* Fix clean:full command

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
This commit is contained in:
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
open -a "$1/$2-darwin-x64/$2.app"
elif [ "$(uname -o)" = "Msys" ]; then
"$1/$2-win32-x64/$2.exe" --verbose
"$1/$2-win32-x64/$2.exe"
else
"$1/$2-linux-x64/$2"
fi

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
module.exports = {
parser: '@typescript-eslint/parser',
parser: base.parser,
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'prettier',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
plugins: base.plugins,
extends: base.extends,
rules: base.rules,
// https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files
ignorePatterns: [
'node_modules/**',
'lib/**',
'dist/**',
'built-tests/**',
'coverage/**',
],
rules: {
'no-console': 'error',
'prettier/prettier': 'error',
// TODO remove when done killing `any`s and making tsc strict
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-template-expressions': 'off'
},
};

View File

@ -12,8 +12,8 @@
],
"scripts": {},
"dependencies": {
"electron-context-menu": "^2.5.0",
"electron-dl": "^3.2.0",
"electron-context-menu": "^3.1.0",
"electron-dl": "^3.2.1",
"electron-squirrel-startup": "^1.0.0",
"electron-window-state": "^5.0.3",
"loglevel": "^1.7.1",

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 { nativeTabsSupported, openExternal } from '../helpers/helpers';
import { setupNativefierWindow } from '../helpers/windowEvents';
import { createNewTab, createNewWindow } from '../helpers/windowHelpers';
import {
OutputOptions,
outputOptionsToWindowOptions,
} from '../../../shared/src/options/model';
export function initContextMenu(options, window?: BrowserWindow): void {
// Require this at runtime, otherwise its child dependency 'electron-is-dev'
// throws an error during unit testing.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const contextMenu = require('electron-context-menu');
export function initContextMenu(
options: OutputOptions,
window?: BrowserWindow,
): void {
log.debug('initContextMenu', { options, window });
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
contextMenu({
prepend: (actions, params) => {
prepend: (actions: contextMenu.Actions, params: ContextMenuParams) => {
log.debug('contextMenu.prepend', { actions, params });
const items = [];
if (params.linkURL) {
@ -29,7 +33,7 @@ export function initContextMenu(options, window?: BrowserWindow): void {
label: 'Open Link in New Window',
click: () =>
createNewWindow(
options,
outputOptionsToWindowOptions(options),
setupNativefierWindow,
params.linkURL,
window,
@ -40,7 +44,7 @@ export function initContextMenu(options, window?: BrowserWindow): void {
label: 'Open Link in New Tab',
click: () =>
createNewTab(
options,
outputOptionsToWindowOptions(options),
setupNativefierWindow,
params.linkURL,
true,

View File

@ -5,7 +5,7 @@ import * as log from 'loglevel';
import { BrowserWindow, ipcMain } from 'electron';
export async function createLoginWindow(
loginCallback,
loginCallback: (username?: string, password?: string) => void,
parent?: BrowserWindow,
): Promise<BrowserWindow> {
log.debug('createLoginWindow', { loginCallback, parent });
@ -25,7 +25,7 @@ export async function createLoginWindow(
`file://${path.join(__dirname, 'static/login.html')}`,
);
ipcMain.once('login-message', (event, usernameAndPassword) => {
ipcMain.once('login-message', (event, usernameAndPassword: string[]) => {
log.debug('login-message', { event, username: usernameAndPassword[0] });
loginCallback(usernameAndPassword[0], usernameAndPassword[1]);
loginWindow.close();

View File

@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { ipcMain, BrowserWindow, IpcMainEvent } from 'electron';
import { ipcMain, BrowserWindow, Event } from 'electron';
import windowStateKeeper from 'electron-window-state';
import log from 'loglevel';
@ -19,6 +19,10 @@ import {
} from '../helpers/windowHelpers';
import { initContextMenu } from './contextMenu';
import { createMenu } from './menu';
import {
OutputOptions,
outputOptionsToWindowOptions,
} from '../../../shared/src/options/model';
export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
@ -32,7 +36,7 @@ type SessionInteractionRequest = {
type SessionInteractionResult = {
id?: string;
value?: unknown;
value?: unknown | Promise<unknown>;
error?: Error;
};
@ -41,7 +45,7 @@ type SessionInteractionResult = {
* @param {function} setDockBadge
*/
export async function createMainWindow(
nativefierOptions,
nativefierOptions: OutputOptions,
setDockBadge: (value: number | string, bounce?: boolean) => void,
): Promise<BrowserWindow> {
const options = { ...nativefierOptions };
@ -66,10 +70,10 @@ export async function createMainWindow(
fullscreen: options.fullScreen,
// Whether the window should always stay on top of other windows. Default is false.
alwaysOnTop: options.alwaysOnTop,
titleBarStyle: options.titleBarStyle,
titleBarStyle: options.titleBarStyle ?? 'default',
show: options.tray !== 'start-in-tray',
backgroundColor: options.backgroundColor,
...getDefaultWindowOptions(options),
...getDefaultWindowOptions(outputOptionsToWindowOptions(options)),
});
mainWindowState.manage(mainWindow);
@ -86,7 +90,7 @@ export async function createMainWindow(
}
createMenu(options, mainWindow);
createContextMenu(options, mainWindow);
setupNativefierWindow(options, mainWindow);
setupNativefierWindow(outputOptionsToWindowOptions(options), mainWindow);
// .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...)
// We can't quite cut over to that yet for a few reasons:
@ -104,7 +108,7 @@ export async function createMainWindow(
'new-window',
(event, url, frameName, disposition) => {
onNewWindow(
options,
outputOptionsToWindowOptions(options),
setupNativefierWindow,
event,
url,
@ -131,20 +135,25 @@ export async function createMainWindow(
await clearCache(mainWindow);
}
if (options.targetUrl) {
await mainWindow.loadURL(options.targetUrl);
}
setupCloseEvent(options, mainWindow);
return mainWindow;
}
function createContextMenu(options, window: BrowserWindow): void {
function createContextMenu(
options: OutputOptions,
window: BrowserWindow,
): void {
if (!options.disableContextMenu) {
initContextMenu(options, window);
}
}
export function saveAppArgs(newAppArgs: any): void {
export function saveAppArgs(newAppArgs: OutputOptions): void {
try {
fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs, null, 2));
} catch (err: unknown) {
@ -154,19 +163,29 @@ export function saveAppArgs(newAppArgs: any): void {
}
}
function setupCloseEvent(options, window: BrowserWindow): void {
window.on('close', (event: IpcMainEvent) => {
function setupCloseEvent(options: OutputOptions, window: BrowserWindow): void {
window.on('close', (event: Event) => {
log.debug('mainWindow.close', event);
if (window.isFullScreen()) {
if (nativeTabsSupported()) {
window.moveTabToNewWindow();
}
window.setFullScreen(false);
window.once('leave-full-screen', (event: IpcMainEvent) =>
hideWindow(window, event, options.fastQuit, options.tray),
window.once('leave-full-screen', (event: Event) =>
hideWindow(
window,
event,
options.fastQuit ?? false,
options.tray ?? 'false',
),
);
}
hideWindow(window, event, options.fastQuit, options.tray);
hideWindow(
window,
event,
options.fastQuit ?? false,
options.tray ?? 'false',
);
if (options.clearCache) {
clearCache(window).catch((err) => log.error('clearCache ERROR', err));
@ -175,7 +194,7 @@ function setupCloseEvent(options, window: BrowserWindow): void {
}
function setupCounter(
options,
options: OutputOptions,
window: BrowserWindow,
setDockBadge: (value: number | string, bounce?: boolean) => void,
): void {
@ -191,7 +210,7 @@ function setupCounter(
}
function setupNotificationBadge(
options,
options: OutputOptions,
window: BrowserWindow,
setDockBadge: (value: number | string, bounce?: boolean) => void,
): void {
@ -208,7 +227,10 @@ function setupNotificationBadge(
});
}
function setupSessionInteraction(options, window: BrowserWindow): void {
function setupSessionInteraction(
options: OutputOptions,
window: BrowserWindow,
): void {
// See API.md / "Accessing The Electron Session"
ipcMain.on(
'session-interaction',
@ -230,14 +252,13 @@ function setupSessionInteraction(options, window: BrowserWindow): void {
}
// Call func with funcArgs
// @ts-expect-error accessing a func by string name
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
result.value = window.webContents.session[request.func](
...request.funcArgs,
);
if (
result.value !== undefined &&
typeof result.value['then'] === 'function'
) {
if (result.value !== undefined && result.value instanceof Promise) {
// This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply
(result.value as Promise<unknown>)
.then((trueResultValue) => {
@ -253,11 +274,13 @@ function setupSessionInteraction(options, window: BrowserWindow): void {
} else if (request.property !== undefined) {
if (request.propertyValue !== undefined) {
// Set the property
// @ts-expect-error setting a property by string name
window.webContents.session[request.property] =
request.propertyValue;
}
// Get the property value
// @ts-expect-error accessing a property by string name
result.value = window.webContents.session[request.property];
} else {
// Why even send the event if you're going to do this? You're just wasting time! ;)

View File

@ -1,4 +1,4 @@
import { BrowserWindow } from 'electron';
import { BrowserWindow, MenuItemConstructorOptions } from 'electron';
jest.mock('../helpers/helpers');
import { isOSX } from '../helpers/helpers';
@ -16,9 +16,15 @@ describe('generateMenu', () => {
beforeEach(() => {
window = new BrowserWindow();
mockIsOSX.mockReset();
mockIsFullScreen = jest.spyOn(window, 'isFullScreen');
mockIsFullScreenable = jest.spyOn(window, 'isFullScreenable');
mockIsSimpleFullScreen = jest.spyOn(window, 'isSimpleFullScreen');
mockIsFullScreen = jest
.spyOn(window, 'isFullScreen')
.mockReturnValue(false);
mockIsFullScreenable = jest
.spyOn(window, 'isFullScreenable')
.mockReturnValue(true);
mockIsSimpleFullScreen = jest
.spyOn(window, 'isSimpleFullScreen')
.mockReturnValue(false);
mockSetFullScreen = jest.spyOn(window, 'setFullScreen');
mockSetSimpleFullScreen = jest.spyOn(window, 'setSimpleFullScreen');
});
@ -46,9 +52,9 @@ describe('generateMenu', () => {
const editMenu = menu.filter((item) => item.label === '&View');
const fullscreen = (editMenu[0].submenu as any[]).filter(
(item) => item.label === 'Toggle Full Screen',
);
const fullscreen = (
editMenu[0].submenu as MenuItemConstructorOptions[]
).filter((item) => item.label === 'Toggle Full Screen');
expect(fullscreen).toHaveLength(1);
expect(fullscreen[0].enabled).toBe(false);
@ -73,9 +79,9 @@ describe('generateMenu', () => {
const editMenu = menu.filter((item) => item.label === '&View');
const fullscreen = (editMenu[0].submenu as any[]).filter(
(item) => item.label === 'Toggle Full Screen',
);
const fullscreen = (
editMenu[0].submenu as MenuItemConstructorOptions[]
).filter((item) => item.label === 'Toggle Full Screen');
expect(fullscreen).toHaveLength(1);
expect(fullscreen[0].enabled).toBe(true);
@ -103,9 +109,9 @@ describe('generateMenu', () => {
const editMenu = menu.filter((item) => item.label === '&View');
const fullscreen = (editMenu[0].submenu as any[]).filter(
(item) => item.label === 'Toggle Full Screen',
);
const fullscreen = (
editMenu[0].submenu as MenuItemConstructorOptions[]
).filter((item) => item.label === 'Toggle Full Screen');
expect(fullscreen).toHaveLength(1);
expect(fullscreen[0].enabled).toBe(true);
@ -114,6 +120,7 @@ describe('generateMenu', () => {
expect(mockIsOSX).toHaveBeenCalled();
expect(mockIsFullScreenable).toHaveBeenCalled();
// @ts-expect-error click is here TypeScript...
fullscreen[0].click(null, window);
expect(mockSetFullScreen).toHaveBeenCalledWith(!isFullScreen);
@ -139,9 +146,9 @@ describe('generateMenu', () => {
const editMenu = menu.filter((item) => item.label === '&View');
const fullscreen = (editMenu[0].submenu as any[]).filter(
(item) => item.label === 'Toggle Full Screen',
);
const fullscreen = (
editMenu[0].submenu as MenuItemConstructorOptions[]
).filter((item) => item.label === 'Toggle Full Screen');
expect(fullscreen).toHaveLength(1);
expect(fullscreen[0].enabled).toBe(true);
@ -150,6 +157,7 @@ describe('generateMenu', () => {
expect(mockIsOSX).toHaveBeenCalled();
expect(mockIsFullScreenable).toHaveBeenCalled();
// @ts-expect-error click is here TypeScript...
fullscreen[0].click(null, window);
expect(mockSetSimpleFullScreen).toHaveBeenCalledWith(!isFullScreen);

View File

@ -21,6 +21,7 @@ import {
zoomOut,
zoomReset,
} from '../helpers/windowHelpers';
import { OutputOptions } from '../../../shared/src/options/model';
type BookmarksLink = {
type: 'link';
@ -40,7 +41,10 @@ type BookmarksMenuConfig = {
bookmarks: BookmarkConfig[];
};
export function createMenu(options, mainWindow: BrowserWindow): void {
export function createMenu(
options: OutputOptions,
mainWindow: BrowserWindow,
): void {
log.debug('createMenu', { options, mainWindow });
const menuTemplate = generateMenu(options, mainWindow);
@ -51,7 +55,11 @@ export function createMenu(options, mainWindow: BrowserWindow): void {
}
export function generateMenu(
options,
options: {
disableDevTools: boolean;
nativefierVersion: string;
zoom?: number;
},
mainWindow: BrowserWindow,
): MenuItemConstructorOptions[] {
const { nativefierVersion, zoom, disableDevTools } = options;
@ -108,7 +116,10 @@ export function generateMenu(
},
{
label: 'Clear App Data',
click: (item: MenuItem, focusedWindow: BrowserWindow): void => {
click: (
item: MenuItem,
focusedWindow: BrowserWindow | undefined,
): void => {
log.debug('Clear App Data.click', {
item,
focusedWindow,
@ -164,7 +175,10 @@ export function generateMenu(
accelerator: isOSX() ? 'Ctrl+Cmd+F' : 'F11',
enabled: mainWindow.isFullScreenable() || isOSX(),
visible: mainWindow.isFullScreenable() || isOSX(),
click: (item: MenuItem, focusedWindow: BrowserWindow): void => {
click: (
item: MenuItem,
focusedWindow: BrowserWindow | undefined,
): void => {
log.debug('Toggle Full Screen.click()', {
item,
focusedWindow,
@ -230,7 +244,7 @@ export function generateMenu(
{
label: 'Toggle Developer Tools',
accelerator: isOSX() ? 'Alt+Cmd+I' : 'Ctrl+Shift+I',
click: (item: MenuItem, focusedWindow: BrowserWindow) => {
click: (item: MenuItem, focusedWindow: BrowserWindow | undefined) => {
log.debug('Toggle Developer Tools.click()', { item, focusedWindow });
if (!focusedWindow) {
focusedWindow = mainWindow;
@ -349,12 +363,11 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
}
try {
const bookmarksMenuConfig: BookmarksMenuConfig = JSON.parse(
const bookmarksMenuConfig = JSON.parse(
fs.readFileSync(bookmarkConfigPath, 'utf-8'),
);
const bookmarksMenu: MenuItemConstructorOptions = {
label: bookmarksMenuConfig.menuLabel,
submenu: bookmarksMenuConfig.bookmarks.map((bookmark) => {
) as BookmarksMenuConfig;
const submenu: MenuItemConstructorOptions[] =
bookmarksMenuConfig.bookmarks.map((bookmark) => {
switch (bookmark.type) {
case 'link':
if (!('title' in bookmark && 'url' in bookmark)) {
@ -370,11 +383,12 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
return {
label: bookmark.title,
click: (): void => {
goToURL(bookmark.url).catch((err: unknown): void =>
goToURL(bookmark.url)?.catch((err: unknown): void =>
log.error(`${bookmark.title}.click ERROR`, err),
);
},
accelerator: 'shortcut' in bookmark ? bookmark.shortcut : null,
accelerator:
'shortcut' in bookmark ? bookmark.shortcut : undefined,
};
case 'separator':
return {
@ -385,7 +399,10 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
'A bookmarks menu entry has an invalid type; type must be one of "link", "separator".',
);
}
}),
});
const bookmarksMenu: MenuItemConstructorOptions = {
label: bookmarksMenuConfig.menuLabel,
submenu,
};
// Insert custom bookmarks menu between menus "View" and "Window"
menuTemplate.splice(menuTemplate.length - 2, 0, bookmarksMenu);

View File

@ -2,15 +2,19 @@ import { app, Tray, Menu, ipcMain, nativeImage, BrowserWindow } from 'electron';
import log from 'loglevel';
import { getAppIcon, getCounterValue, isOSX } from '../helpers/helpers';
import { OutputOptions } from '../../../shared/src/options/model';
export function createTrayIcon(
nativefierOptions,
nativefierOptions: OutputOptions,
mainWindow: BrowserWindow,
): Tray {
): Tray | undefined {
const options = { ...nativefierOptions };
if (options.tray && options.tray !== 'false') {
const iconPath = getAppIcon();
if (!iconPath) {
throw new Error('Icon path not found found to use with tray option.');
}
const nimage = nativeImage.createFromPath(iconPath);
const appIcon = new Tray(nativeImage.createEmpty());
@ -39,7 +43,7 @@ export function createTrayIcon(
},
{
label: 'Quit',
click: app.exit.bind(this),
click: (): void => app.exit(0),
},
]);
@ -50,9 +54,11 @@ export function createTrayIcon(
log.debug('mainWindow.page-title-updated', { event, title });
const counterValue = getCounterValue(title);
if (counterValue) {
appIcon.setToolTip(`(${counterValue}) ${options.name}`);
appIcon.setToolTip(
`(${counterValue}) ${options.name ?? 'Nativefier'}`,
);
} else {
appIcon.setToolTip(options.name);
appIcon.setToolTip(options.name ?? '');
}
});
} else {
@ -61,20 +67,22 @@ export function createTrayIcon(
if (mainWindow.isFocused()) {
return;
}
if (options.name) {
appIcon.setToolTip(`${options.name}`);
}
});
mainWindow.on('focus', () => {
log.debug('mainWindow.focus');
appIcon.setToolTip(options.name);
appIcon.setToolTip(options.name ?? '');
});
}
appIcon.setToolTip(options.name);
appIcon.setToolTip(options.name ?? '');
appIcon.setContextMenu(contextMenu);
return appIcon;
}
return null;
return undefined;
}

View File

@ -39,7 +39,7 @@ function domainify(url: string): string {
return domain;
}
export function getAppIcon(): string {
export function getAppIcon(): string | undefined {
// Prefer ICO under Windows, see
// https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions
// https://www.electronjs.org/docs/api/native-image#supported-formats
@ -55,7 +55,7 @@ export function getAppIcon(): string {
}
}
export function getCounterValue(title: string): string {
export function getCounterValue(title: string): string | undefined {
const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/;
const match = itemCountRegex.exec(title);
return match ? match[1] : undefined;
@ -74,7 +74,7 @@ export function getCSSToInject(): string {
for (const cssFile of cssFiles) {
log.debug('Injecting CSS file', cssFile);
const cssFileData = fs.readFileSync(cssFile);
cssToInject += `/* ${cssFile} */\n\n ${cssFileData}\n\n`;
cssToInject += `/* ${cssFile} */\n\n ${cssFileData.toString()}\n\n`;
}
return cssToInject;
}
@ -114,7 +114,7 @@ function isInternalLoginPage(url: string): boolean {
export function linkIsInternal(
currentUrl: string,
newUrl: string,
internalUrlRegex: string | RegExp,
internalUrlRegex: string | RegExp | undefined,
): boolean {
log.debug('linkIsInternal', { currentUrl, newUrl, internalUrlRegex });
if (newUrl.split('#')[0] === 'about:blank') {

View File

@ -72,7 +72,7 @@ function findFlashOnMac(): string {
)[0];
}
export function inferFlashPath(): string {
export function inferFlashPath(): string | undefined {
if (isOSX()) {
return findFlashOnMac();
}
@ -86,5 +86,5 @@ export function inferFlashPath(): string {
}
log.warn('Unable to determine OS to infer flash player');
return null;
return undefined;
}

View File

@ -3,9 +3,33 @@ jest.mock('./windowEvents');
jest.mock('./windowHelpers');
import { dialog, BrowserWindow, WebContents } from 'electron';
import { WindowOptions } from '../../../shared/src/options/model';
import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers';
const { onNewWindowHelper, onWillNavigate, onWillPreventUnload } =
jest.requireActual('./windowEvents');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {
onNewWindowHelper,
onWillNavigate,
onWillPreventUnload,
}: {
onNewWindowHelper: (
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
urlToGo: string,
disposition: string | undefined,
preventDefault: (newGuest: BrowserWindow) => void,
parent?: BrowserWindow,
) => Promise<void>;
onWillNavigate: (
options: {
blockExternalUrls: boolean;
internalUrls?: string | RegExp;
targetUrl: string;
},
event: unknown,
urlToGo: string,
) => Promise<void>;
onWillPreventUnload: (event: unknown) => void;
} = jest.requireActual('./windowEvents');
import {
blockExternalURL,
createAboutBlankWindow,
@ -18,7 +42,13 @@ describe('onNewWindowHelper', () => {
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
const foregroundDisposition = 'foreground-tab';
const backgroundDisposition = 'background-tab';
const baseOptions = {
blockExternalUrls: false,
insecure: false,
name: 'TEST_APP',
targetUrl: originalURL,
zoom: 1.0,
};
const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock;
const mockCreateAboutBlank: jest.SpyInstance =
createAboutBlankWindow as jest.Mock;
@ -54,14 +84,9 @@ describe('onNewWindowHelper', () => {
mockOpenExternal.mockRestore();
});
test('internal urls should not be handled', () => {
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
test('internal urls should not be handled', async () => {
await onNewWindowHelper(
baseOptions,
setupWindow,
internalURL,
undefined,
@ -75,14 +100,11 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).not.toHaveBeenCalled();
});
test('external urls should be opened externally', () => {
test('external urls should be opened externally', async () => {
mockLinkIsInternal.mockReturnValue(false);
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
await onNewWindowHelper(
baseOptions,
setupWindow,
externalURL,
undefined,
@ -96,13 +118,13 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('external urls should be ignored if blockExternalUrls is true', () => {
test('external urls should be ignored if blockExternalUrls is true', async () => {
mockLinkIsInternal.mockReturnValue(false);
const options = {
...baseOptions,
blockExternalUrls: true,
targetUrl: originalURL,
};
onNewWindowHelper(
await onNewWindowHelper(
options,
setupWindow,
externalURL,
@ -117,13 +139,9 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('tab disposition should be ignored if tabs are not enabled', () => {
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
test('tab disposition should be ignored if tabs are not enabled', async () => {
await onNewWindowHelper(
baseOptions,
setupWindow,
internalURL,
foregroundDisposition,
@ -137,14 +155,11 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).not.toHaveBeenCalled();
});
test('tab disposition should be ignored if url is external', () => {
test('tab disposition should be ignored if url is external', async () => {
mockLinkIsInternal.mockReturnValue(false);
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
await onNewWindowHelper(
baseOptions,
setupWindow,
externalURL,
foregroundDisposition,
@ -158,15 +173,11 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('foreground tabs with internal urls should be opened in the foreground', () => {
test('foreground tabs with internal urls should be opened in the foreground', async () => {
mockNativeTabsSupported.mockReturnValue(true);
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
await onNewWindowHelper(
baseOptions,
setupWindow,
internalURL,
foregroundDisposition,
@ -176,7 +187,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).toHaveBeenCalledWith(
options,
baseOptions,
setupWindow,
internalURL,
true,
@ -187,15 +198,11 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('background tabs with internal urls should be opened in background tabs', () => {
test('background tabs with internal urls should be opened in background tabs', async () => {
mockNativeTabsSupported.mockReturnValue(true);
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
await onNewWindowHelper(
baseOptions,
setupWindow,
internalURL,
backgroundDisposition,
@ -205,7 +212,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).toHaveBeenCalledWith(
options,
baseOptions,
setupWindow,
internalURL,
false,
@ -216,13 +223,9 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('about:blank urls should be handled', () => {
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
test('about:blank urls should be handled', async () => {
await onNewWindowHelper(
baseOptions,
setupWindow,
'about:blank',
undefined,
@ -236,13 +239,9 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('about:blank#blocked urls should be handled', () => {
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
test('about:blank#blocked urls should be handled', async () => {
await onNewWindowHelper(
baseOptions,
setupWindow,
'about:blank#blocked',
undefined,
@ -256,13 +255,9 @@ describe('onNewWindowHelper', () => {
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('about:blank#other urls should not be handled', () => {
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
onNewWindowHelper(
options,
test('about:blank#other urls should not be handled', async () => {
await onNewWindowHelper(
baseOptions,
setupWindow,
'about:blank#other',
undefined,
@ -302,40 +297,40 @@ describe('onWillNavigate', () => {
mockOpenExternal.mockRestore();
});
test('internal urls should not be handled', () => {
test('internal urls should not be handled', async () => {
mockLinkIsInternal.mockReturnValue(true);
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
const event = { preventDefault };
onWillNavigate(options, event, internalURL);
await onWillNavigate(options, event, internalURL);
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled();
});
test('external urls should be opened externally', () => {
test('external urls should be opened externally', async () => {
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
const event = { preventDefault };
onWillNavigate(options, event, externalURL);
await onWillNavigate(options, event, externalURL);
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('external urls should be ignored if blockExternalUrls is true', () => {
test('external urls should be ignored if blockExternalUrls is true', async () => {
const options = {
blockExternalUrls: true,
targetUrl: originalURL,
};
const event = { preventDefault };
onWillNavigate(options, event, externalURL);
await onWillNavigate(options, event, externalURL);
expect(mockBlockExternalURL).toHaveBeenCalledTimes(1);
expect(mockOpenExternal).not.toHaveBeenCalled();

View File

@ -1,11 +1,12 @@
import {
dialog,
BrowserWindow,
IpcMainEvent,
Event,
NewWindowWebContentsEvent,
WebContents,
} from 'electron';
import log from 'loglevel';
import { WindowOptions } from '../../../shared/src/options/model';
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
import {
@ -18,8 +19,8 @@ import {
} from './windowHelpers';
export function onNewWindow(
options,
setupWindow: (...args) => void,
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
event: NewWindowWebContentsEvent,
urlToGo: string,
frameName: string,
@ -39,7 +40,7 @@ export function onNewWindow(
disposition,
parent,
});
const preventDefault = (newGuest: BrowserWindow): void => {
const preventDefault = (newGuest?: BrowserWindow): void => {
log.debug('onNewWindow.preventDefault', { newGuest, event });
event.preventDefault();
if (newGuest) {
@ -57,14 +58,15 @@ export function onNewWindow(
}
export function onNewWindowHelper(
options,
setupWindow: (...args) => void,
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
urlToGo: string,
disposition: string,
preventDefault,
disposition: string | undefined,
preventDefault: (newGuest?: BrowserWindow) => void,
parent?: BrowserWindow,
): Promise<void> {
log.debug('onNewWindowHelper', {
options,
urlToGo,
disposition,
preventDefault,
@ -74,7 +76,13 @@ export function onNewWindowHelper(
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
preventDefault();
if (options.blockExternalUrls) {
return blockExternalURL(urlToGo).then(() => null);
return new Promise((resolve) => {
blockExternalURL(urlToGo)
.then(() => resolve())
.catch((err: unknown) => {
throw err;
});
});
} else {
return openExternal(urlToGo);
}
@ -108,15 +116,21 @@ export function onNewWindowHelper(
}
export function onWillNavigate(
options,
event: IpcMainEvent,
options: WindowOptions,
event: Event,
urlToGo: string,
): Promise<void> {
log.debug('onWillNavigate', { options, event, urlToGo });
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
event.preventDefault();
if (options.blockExternalUrls) {
return blockExternalURL(urlToGo).then(() => null);
return new Promise((resolve) => {
blockExternalURL(urlToGo)
.then(() => resolve())
.catch((err: unknown) => {
throw err;
});
});
} else {
return openExternal(urlToGo);
}
@ -124,19 +138,25 @@ export function onWillNavigate(
return Promise.resolve(undefined);
}
export function onWillPreventUnload(event: IpcMainEvent): void {
export function onWillPreventUnload(
event: Event & { sender?: WebContents },
): void {
log.debug('onWillPreventUnload', event);
const webContents: WebContents = event.sender;
if (webContents === undefined) {
const webContents = event.sender;
if (!webContents) {
return;
}
const browserWindow = BrowserWindow.fromWebContents(webContents);
const browserWindow =
BrowserWindow.fromWebContents(webContents) ??
BrowserWindow.getFocusedWindow();
if (browserWindow) {
const choice = dialog.showMessageBoxSync(browserWindow, {
type: 'question',
buttons: ['Proceed', 'Stay'],
message: 'You may have unsaved changes, are you sure you want to proceed?',
message:
'You may have unsaved changes, are you sure you want to proceed?',
title: 'Changes you made may not be saved.',
defaultId: 0,
cancelId: 1,
@ -145,8 +165,12 @@ export function onWillPreventUnload(event: IpcMainEvent): void {
event.preventDefault();
}
}
}
export function setupNativefierWindow(options, window: BrowserWindow): void {
export function setupNativefierWindow(
options: WindowOptions,
window: BrowserWindow,
): void {
if (options.userAgent) {
window.webContents.userAgent = options.userAgent;
}
@ -157,7 +181,7 @@ export function setupNativefierWindow(options, window: BrowserWindow): void {
injectCSS(window);
window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => {
window.webContents.on('will-navigate', (event: Event, url: string) => {
onWillNavigate(options, event, url).catch((err) => {
log.error('window.webContents.on.will-navigate ERROR', err);
event.preventDefault();

View File

@ -6,6 +6,7 @@ import {
} from 'electron';
jest.mock('loglevel');
import { error } from 'loglevel';
import { WindowOptions } from '../../../shared/src/options/model';
jest.mock('./helpers');
import { getCSSToInject } from './helpers';
@ -57,7 +58,13 @@ describe('clearAppData', () => {
describe('createNewTab', () => {
const window = new BrowserWindow();
const options = {};
const options: WindowOptions = {
blockExternalUrls: false,
insecure: false,
name: 'Test App',
targetUrl: 'https://github.com/nativefier/natifefier',
zoom: 1.0,
};
const setupWindow = jest.fn();
const url = 'https://github.com/nativefier/nativefier';
const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn(
@ -100,6 +107,7 @@ describe('injectCSS', () => {
jest.setTimeout(10000);
const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock;
let mockGetURL: jest.SpyInstance;
const mockLogError: jest.SpyInstance = error as jest.Mock;
const mockWebContentsInsertCSS: jest.SpyInstance = jest.spyOn(
WebContents.prototype,
@ -107,17 +115,21 @@ describe('injectCSS', () => {
);
const css = 'body { color: white; }';
let responseHeaders;
let responseHeaders: Record<string, string[]>;
beforeEach(() => {
mockGetCSSToInject.mockReset().mockReturnValue('');
mockGetURL = jest
.spyOn(WebContents.prototype, 'getURL')
.mockReturnValue('https://example.com');
mockLogError.mockReset();
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
responseHeaders = { 'x-header': 'value', 'content-type': ['test/other'] };
responseHeaders = { 'x-header': ['value'], 'content-type': ['test/other'] };
});
afterAll(() => {
mockGetCSSToInject.mockRestore();
mockGetURL.mockRestore();
mockLogError.mockRestore();
mockWebContentsInsertCSS.mockRestore();
});
@ -141,6 +153,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate');
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send(
'onHeadersReceived',
{ responseHeaders, webContents: window.webContents },
@ -168,6 +181,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate');
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send(
'onHeadersReceived',
{ responseHeaders, webContents: window.webContents },
@ -190,6 +204,11 @@ describe('injectCSS', () => {
'image/png',
])(
'will not inject for content-type %s',
// @ts-expect-error because TypeScript can't recognize that
// '(contentType: string, done: jest.DoneCallback) => void'
// and
// '(...args: (string | DoneCallback)[]) => any'
// are actually compatible.
(contentType: string, done: jest.DoneCallback) => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
@ -203,6 +222,7 @@ describe('injectCSS', () => {
expect(window.webContents.emit('did-navigate')).toBe(true);
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send(
'onHeadersReceived',
{
@ -223,6 +243,11 @@ describe('injectCSS', () => {
test.each<string | jest.DoneCallback>(['text/html'])(
'will inject for content-type %s',
// @ts-expect-error because TypeScript can't recognize that
// '(contentType: string, done: jest.DoneCallback) => void'
// and
// '(...args: (string | DoneCallback)[]) => any'
// are actually compatible.
(contentType: string, done: jest.DoneCallback) => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
@ -236,6 +261,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send(
'onHeadersReceived',
{
@ -260,6 +286,11 @@ describe('injectCSS', () => {
'xhr',
])(
'will not inject for resource type %s',
// @ts-expect-error because TypeScript can't recognize that
// '(contentType: string, done: jest.DoneCallback) => void'
// and
// '(...args: (string | DoneCallback)[]) => any'
// are actually compatible.
(resourceType: string, done: jest.DoneCallback) => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
@ -271,6 +302,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send(
'onHeadersReceived',
{
@ -292,6 +324,11 @@ describe('injectCSS', () => {
test.each<string | jest.DoneCallback>(['html', 'other'])(
'will inject for resource type %s',
// @ts-expect-error because TypeScript can't recognize that
// '(contentType: string, done: jest.DoneCallback) => void'
// and
// '(...args: (string | DoneCallback)[]) => any'
// are actually compatible.
(resourceType: string, done: jest.DoneCallback) => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
@ -303,6 +340,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send(
'onHeadersReceived',
{

View File

@ -2,14 +2,16 @@ import {
dialog,
BrowserWindow,
BrowserWindowConstructorOptions,
Event,
HeadersReceivedResponse,
IpcMainEvent,
MessageBoxReturnValue,
OnHeadersReceivedListenerDetails,
WebPreferences,
} from 'electron';
import log from 'loglevel';
import path from 'path';
import { TrayValue, WindowOptions } from '../../../shared/src/options/model';
import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers';
const ZOOM_INTERVAL = 0.1;
@ -61,8 +63,8 @@ export async function clearCache(window: BrowserWindow): Promise<void> {
}
export function createAboutBlankWindow(
options,
setupWindow: (...args) => void,
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
parent?: BrowserWindow,
): BrowserWindow {
const window = createNewWindow(options, setupWindow, 'about:blank', parent);
@ -78,12 +80,12 @@ export function createAboutBlankWindow(
}
export function createNewTab(
options,
setupWindow,
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
url: string,
foreground: boolean,
parent?: BrowserWindow,
): BrowserWindow {
): BrowserWindow | undefined {
log.debug('createNewTab', { url, foreground, parent });
return withFocusedWindow((focusedWindow) => {
const newTab = createNewWindow(options, setupWindow, url, parent);
@ -96,8 +98,8 @@ export function createNewTab(
}
export function createNewWindow(
options,
setupWindow: (...args) => void,
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
url: string,
parent?: BrowserWindow,
): BrowserWindow {
@ -118,7 +120,7 @@ export function getCurrentURL(): string {
}
export function getDefaultWindowOptions(
options,
options: WindowOptions,
): BrowserWindowConstructorOptions {
const browserwindowOptions: BrowserWindowConstructorOptions = {
...options.browserwindowOptions,
@ -128,7 +130,7 @@ export function getDefaultWindowOptions(
// webPreferences specified in the DEFAULT_WINDOW_OPTIONS with itself
delete browserwindowOptions.webPreferences;
const webPreferences = {
const webPreferences: WebPreferences = {
...(options.browserwindowOptions?.webPreferences ?? {}),
};
@ -171,15 +173,15 @@ export function goForward(): void {
});
}
export function goToURL(url: string): Promise<void> {
export function goToURL(url: string): Promise<void> | undefined {
return withFocusedWindow((focusedWindow) => focusedWindow.loadURL(url));
}
export function hideWindow(
window: BrowserWindow,
event: IpcMainEvent,
event: Event,
fastQuit: boolean,
tray: 'true' | 'false' | 'start-in-tray',
tray: TrayValue,
): void {
if (isOSX() && !fastQuit) {
// this is called when exiting from clicking the cross button on the window
@ -221,7 +223,7 @@ export function injectCSS(browserWindow: BrowserWindow): void {
callback: (headersReceivedResponse: HeadersReceivedResponse) => void,
) => {
const contentType =
'content-type' in details.responseHeaders
details.responseHeaders && 'content-type' in details.responseHeaders
? details.responseHeaders['content-type'][0]
: undefined;
@ -252,9 +254,9 @@ export function injectCSS(browserWindow: BrowserWindow): void {
async function injectCSSIntoResponse(
details: OnHeadersReceivedListenerDetails,
contentType: string,
contentType: string | undefined,
cssToInject: string,
): Promise<Record<string, string[]>> {
): Promise<Record<string, string[]> | undefined> {
// We go with a denylist rather than a whitelist (e.g. only text/html)
// to avoid "whoops I didn't think this should have been CSS-injected" cases
const nonInjectableContentTypes = [
@ -265,19 +267,26 @@ async function injectCSSIntoResponse(
const nonInjectableResourceTypes = ['image', 'script', 'stylesheet', 'xhr'];
if (
nonInjectableContentTypes.filter((x) => x.exec(contentType)?.length > 0)
?.length > 0 ||
(contentType &&
nonInjectableContentTypes.filter((x) => {
const matches = x.exec(contentType);
return matches && matches?.length > 0;
})?.length > 0) ||
nonInjectableResourceTypes.includes(details.resourceType) ||
!details.webContents
) {
log.debug(
`Skipping CSS injection for:\n${details.url}\nwith resourceType ${details.resourceType} and content-type ${contentType}`,
`Skipping CSS injection for:\n${details.url}\nwith resourceType ${
details.resourceType
} and content-type ${contentType as string}`,
);
return details.responseHeaders;
}
log.debug(
`Injecting CSS for:\n${details.url}\nwith resourceType ${details.resourceType} and content-type ${contentType}`,
`Injecting CSS for:\n${details.url}\nwith resourceType ${
details.resourceType
} and content-type ${contentType as string}`,
);
await details.webContents.insertCSS(cssToInject);
@ -285,7 +294,7 @@ async function injectCSSIntoResponse(
}
export function sendParamsOnDidFinishLoad(
options,
options: WindowOptions,
window: BrowserWindow,
): void {
window.webContents.on('did-finish-load', () => {
@ -304,7 +313,10 @@ export function sendParamsOnDidFinishLoad(
});
}
export function setProxyRules(window: BrowserWindow, proxyRules): void {
export function setProxyRules(
window: BrowserWindow,
proxyRules?: string,
): void {
window.webContents.session
.setProxy({
proxyRules,
@ -314,13 +326,15 @@ export function setProxyRules(window: BrowserWindow, proxyRules): void {
.catch((err) => log.error('session.setProxy ERROR', err));
}
export function withFocusedWindow<T>(block: (window: BrowserWindow) => T): T {
export function withFocusedWindow<T>(
block: (window: BrowserWindow) => T,
): T | undefined {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
return block(focusedWindow);
}
return null;
return undefined;
}
export function zoomOut(): void {
@ -328,7 +342,7 @@ export function zoomOut(): void {
adjustWindowZoom(-ZOOM_INTERVAL);
}
export function zoomReset(options): void {
export function zoomReset(options: { zoom?: number }): void {
log.debug('zoomReset');
withFocusedWindow((focusedWindow) => {
focusedWindow.webContents.zoomFactor = options.zoom ?? 1.0;

View File

@ -10,7 +10,7 @@ import electron, {
globalShortcut,
systemPreferences,
BrowserWindow,
IpcMainEvent,
Event,
} from 'electron';
import electronDownload from 'electron-dl';
import * as log from 'loglevel';
@ -25,6 +25,10 @@ import { createTrayIcon } from './components/trayIcon';
import { isOSX, removeUserAgentSpecifics } from './helpers/helpers';
import { inferFlashPath } from './helpers/inferFlash';
import { setupNativefierWindow } from './helpers/windowEvents';
import {
OutputOptions,
outputOptionsToWindowOptions,
} from '../../shared/src/options/model';
// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
if (require('electron-squirrel-startup')) {
@ -39,7 +43,9 @@ if (process.argv.indexOf('--verbose') > -1) {
let mainWindow: BrowserWindow;
const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'));
const appArgs = JSON.parse(
fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'),
) as OutputOptions;
log.debug('appArgs', appArgs);
// Do this relatively early so that we can start storing appData with the app
@ -94,8 +100,11 @@ if (appArgs.processEnvs) {
if (typeof appArgs.processEnvs === 'string') {
process.env.processEnvs = appArgs.processEnvs;
} else {
Object.keys(appArgs.processEnvs).forEach((key) => {
/* eslint-env node */
Object.keys(appArgs.processEnvs)
.filter((key) => key !== undefined)
.forEach((key) => {
// @ts-expect-error TS will complain this could be undefined, but we filtered those out
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
process.env[key] = appArgs.processEnvs[key];
});
}
@ -125,7 +134,10 @@ if (appArgs.enableEs3Apis) {
}
if (appArgs.diskCacheSize) {
app.commandLine.appendSwitch('disk-cache-size', appArgs.diskCacheSize);
app.commandLine.appendSwitch(
'disk-cache-size',
appArgs.diskCacheSize.toString(),
);
}
if (appArgs.basicAuthUsername) {
@ -143,7 +155,7 @@ if (appArgs.basicAuthPassword) {
}
if (appArgs.lang) {
const langParts = (appArgs.lang as string).split(',');
const langParts = appArgs.lang.split(',');
// Convert locales to languages, because for some reason locales don't work. Stupid Chromium
const langPartsParsed = Array.from(
// Convert to set to dedupe in case something like "en-GB,en-US" was passed
@ -156,10 +168,12 @@ if (appArgs.lang) {
let currentBadgeCount = 0;
const setDockBadge = isOSX()
? (count: number, bounce = false): void => {
? (count?: number | string, bounce = false): void => {
if (count) {
app.dock.setBadge(count.toString());
if (bounce && count > currentBadgeCount) app.dock.bounce();
currentBadgeCount = count;
currentBadgeCount = typeof count === 'number' ? count : 0;
}
}
: (): void => undefined;
@ -191,17 +205,17 @@ app.on('quit', (event, exitCode) => {
log.debug('app.quit', { event, exitCode });
});
if (appArgs.crashReporter) {
app.on('will-finish-launching', () => {
log.debug('app.will-finish-launching');
if (appArgs.crashReporter) {
crashReporter.start({
companyName: appArgs.companyName || '',
companyName: appArgs.companyName ?? '',
productName: appArgs.name,
submitURL: appArgs.crashReporter,
uploadToServer: true,
});
});
}
});
if (appArgs.widevine) {
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
@ -270,7 +284,15 @@ app.on('new-window-for-tab', () => {
}
});
app.on('login', (event, webContents, request, authInfo, callback) => {
app.on(
'login',
(
event,
webContents,
request,
authInfo,
callback: (username?: string, password?: string) => void,
) => {
log.debug('app.login', { event, request });
// for http authentication
event.preventDefault();
@ -282,7 +304,8 @@ app.on('login', (event, webContents, request, authInfo, callback) => {
log.error('createLoginWindow ERROR', err),
);
}
});
},
);
async function onReady(): Promise<void> {
// Warning: `mainWindow` below is the *global* unique `mainWindow`, created at init time
@ -295,6 +318,7 @@ async function onReady(): Promise<void> {
appArgs.globalShortcuts.forEach((shortcut) => {
globalShortcut.register(shortcut.key, () => {
shortcut.inputEvents.forEach((inputEvent) => {
// @ts-expect-error without including electron in our models, these will never match
mainWindow.webContents.sendInputEvent(inputEvent);
});
});
@ -319,7 +343,9 @@ async function onReady(): Promise<void> {
// the user for permission on Mac.
// For reference:
// https://www.electronjs.org/docs/api/global-shortcut?q=MediaPlayPause#globalshortcutregisteraccelerator-callback
const accessibilityPromptResult = dialog.showMessageBoxSync(null, {
const accessibilityPromptResult = dialog.showMessageBoxSync(
mainWindow,
{
type: 'question',
message: 'Accessibility Permissions Needed',
buttons: ['Yes', 'No', 'No and never ask again'],
@ -328,7 +354,8 @@ async function onReady(): Promise<void> {
`${appArgs.name} would like to use one or more of your keyboard's media keys (start, stop, next track, or previous track) to control it.\n\n` +
`Would you like Mac OS to ask for your permission to do so?\n\n` +
`If so, you will need to restart ${appArgs.name} after granting permissions for these keyboard shortcuts to begin working.`,
});
},
);
switch (accessibilityPromptResult) {
// User clicked Yes, prompt for accessibility
case 0:
@ -354,7 +381,7 @@ async function onReady(): Promise<void> {
appArgs.oldBuildWarningText ||
'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is insecure to keep using an old version of it. Please upgrade Nativefier and rebuild this app.';
dialog
.showMessageBox(null, {
.showMessageBox(mainWindow, {
type: 'warning',
message: 'Old build detected',
detail: oldBuildWarningText,
@ -365,7 +392,7 @@ async function onReady(): Promise<void> {
app.on(
'accessibility-support-changed',
(event: IpcMainEvent, accessibilitySupportEnabled: boolean) => {
(event: Event, accessibilitySupportEnabled: boolean) => {
log.debug('app.accessibility-support-changed', {
event,
accessibilitySupportEnabled,
@ -375,23 +402,20 @@ app.on(
app.on(
'activity-was-continued',
(event: IpcMainEvent, type: string, userInfo: unknown) => {
(event: Event, type: string, userInfo: unknown) => {
log.debug('app.activity-was-continued', { event, type, userInfo });
},
);
app.on('browser-window-blur', (event: IpcMainEvent, window: BrowserWindow) => {
app.on('browser-window-blur', (event: Event, window: BrowserWindow) => {
log.debug('app.browser-window-blur', { event, window });
});
app.on(
'browser-window-created',
(event: IpcMainEvent, window: BrowserWindow) => {
app.on('browser-window-created', (event: Event, window: BrowserWindow) => {
log.debug('app.browser-window-created', { event, window });
setupNativefierWindow(appArgs, window);
},
);
setupNativefierWindow(outputOptionsToWindowOptions(appArgs), window);
});
app.on('browser-window-focus', (event: IpcMainEvent, window: BrowserWindow) => {
app.on('browser-window-focus', (event: Event, window: BrowserWindow) => {
log.debug('app.browser-window-focus', { event, window });
});

View File

@ -23,6 +23,7 @@ class MockBrowserWindow extends EventEmitter {
webContents: MockWebContents;
constructor(options?: unknown) {
// @ts-expect-error options is really EventEmitterOptions, but events.d.ts doesn't expose it...
super(options);
this.webContents = new MockWebContents();
}
@ -44,15 +45,15 @@ class MockBrowserWindow extends EventEmitter {
}
isSimpleFullScreen(): boolean {
return undefined;
throw new Error('Not implemented');
}
isFullScreen(): boolean {
return undefined;
throw new Error('Not implemented');
}
isFullScreenable(): boolean {
return undefined;
throw new Error('Not implemented');
}
loadURL(url: string, options?: unknown): Promise<void> {
@ -73,14 +74,14 @@ class MockDialog {
browserWindow: MockBrowserWindow,
options: unknown,
): Promise<number> {
return Promise.resolve(undefined);
throw new Error('Not implemented');
}
static showMessageBoxSync(
browserWindow: MockBrowserWindow,
options: unknown,
): number {
return undefined;
throw new Error('Not implemented');
}
}
@ -110,11 +111,11 @@ class MockWebContents extends EventEmitter {
}
getURL(): string {
return undefined;
throw new Error('Not implemented');
}
insertCSS(css: string, options?: unknown): Promise<string> {
return Promise.resolve(undefined);
throw new Error('Not implemented');
}
}
@ -134,6 +135,7 @@ class MockWebRequest {
) => void)
| null,
): void {
if (listener) {
this.emitter.addListener(
'onHeadersReceived',
(
@ -142,6 +144,7 @@ class MockWebRequest {
) => listener(details, callback),
);
}
}
send(event: string, ...args: unknown[]): void {
this.emitter.emit(event, ...args);

View File

@ -11,6 +11,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { ipcRenderer } from 'electron';
import { OutputOptions } from '../../shared/src/options/model';
// Do *NOT* add 3rd-party imports here in preload (except for webpack `externals` like electron).
// They will work during development, but break in the prod build :-/ .
@ -56,7 +57,7 @@ function setNotificationCallback(
get: () => OldNotify.permission,
});
// @ts-expect-error
// @ts-expect-error TypeScript says its not compatible, but it works?
window.Notification = newNotify;
}
@ -92,14 +93,15 @@ function notifyNotificationClick(): void {
ipcRenderer.send('notification-click');
}
// @ts-expect-error TypeScript thinks these are incompatible but they aren't
setNotificationCallback(notifyNotificationCreate, notifyNotificationClick);
ipcRenderer.on('params', (event, message) => {
ipcRenderer.on('params', (event, message: string) => {
log.debug('ipcRenderer.params', { event, message });
const appArgs = JSON.parse(message);
const appArgs = JSON.parse(message) as OutputOptions;
log.info('nativefier.json', appArgs);
});
ipcRenderer.on('debug', (event, message) => {
ipcRenderer.on('debug', (event, message: string) => {
log.debug('ipcRenderer.debug', { event, message });
});

View File

@ -1,15 +1,7 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"allowJs": true,
"declaration": false,
"esModuleInterop": true,
"incremental": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
// Here in app/tsconfig.json, we want to set the `target` and `lib` keys to
// the "best" values for the version of Node **coming with the chosen Electron**.
// Careful: we're *not* talking about Nativefier's (CLI) required Node version,
@ -28,9 +20,17 @@
// 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"]
"lib": [
"es2018",
"dom"
]
},
"include": [
"./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
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'eslint:recommended',

View File

@ -33,15 +33,15 @@
"scripts": {
"build-app": "cd app && webpack",
"build-app-static": "ncp app/src/static/ app/lib/static/ && ncp app/dist/preload.js app/lib/preload.js && ncp app/dist/preload.js.map app/lib/preload.js.map",
"build": "npm run clean && tsc --build . app && npm run build-app && npm run build-app-static",
"build": "npm run clean && tsc --build shared src app && npm run build-app && npm run build-app-static",
"build:watch": "npm run clean && tsc --build . app --watch",
"changelog": "./.github/generate-changelog",
"ci": "npm run lint && npm test",
"clean": "rimraf coverage/ lib/ app/lib/ app/dist/",
"clean:full": "rimraf coverage/ lib/ app/lib/ app/dist/ app/node_modules/ node_modules/",
"lint:fix": "eslint . --ext .ts --fix",
"lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts'",
"lint": "eslint . --ext .ts",
"clean": "rimraf coverage/ lib/ app/lib/ app/dist/ shared/lib",
"clean:full": "npm run clean && rimraf app/node_modules/ node_modules/",
"lint:fix": "cd src && eslint . --ext .ts --fix && cd ../shared && eslint src --ext .ts --fix && cd ../app && eslint src --ext .ts --fix",
"lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts' 'shared/src/**/*.ts'",
"lint": "eslint shared app src --ext .ts",
"list-outdated-deps": "npm out; cd app && npm out; true",
"prepare": "cd app && npm install && cd .. && npm run build",
"test:integration": "jest --testRegex '.*integration-test.js'",
@ -109,10 +109,11 @@
],
"watchPathIgnorePatterns": [
"<rootDir>/src.*",
"<rootDir>/tsconfig.json",
"<rootDir>/tsconfig-base.json",
"<rootDir>/app/src.*",
"<rootDir>/app/lib.*",
"<rootDir>/app/tsconfig.json"
"<rootDir>/app/tsconfig.json",
"<rootDir>/shared/tsconfig.json"
]
},
"prettier": {

14
shared/.eslintrc.js Normal file
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 * as electronPackager from 'electron-packager';
export type TitleBarValue =
| 'default'
| 'hidden'
| 'hiddenInset'
| 'customButtonsOnHover';
export type TrayValue = 'true' | 'false' | 'start-in-tray';
export interface ElectronPackagerOptions extends electronPackager.Options {
@ -34,7 +39,7 @@ export interface AppOptions {
electronVersionUsed?: string;
enableEs3Apis: boolean;
fastQuit: boolean;
fileDownloadOptions: unknown;
fileDownloadOptions?: Record<string, unknown>;
flashPluginDir?: string;
fullScreen: boolean;
globalShortcuts?: GlobalShortcut[];
@ -46,12 +51,12 @@ export interface AppOptions {
internalUrls?: string;
lang?: string;
maximize: boolean;
nativefierVersion?: string;
nativefierVersion: string;
processEnvs?: string;
proxyRules?: string;
showMenuBar: boolean;
singleInstance: boolean;
titleBarStyle?: string;
titleBarStyle?: TitleBarValue;
tray: TrayValue;
userAgent?: string;
userAgentHonest: boolean;
@ -70,10 +75,26 @@ export interface AppOptions {
};
}
export type BrowserWindowOptions = Record<string, unknown>;
export type BrowserWindowOptions = Record<string, unknown> & {
webPreferences?: Record<string, unknown>;
};
export type GlobalShortcut = {
key: string;
inputEvents: {
type:
| 'mouseDown'
| 'mouseUp'
| 'mouseEnter'
| 'mouseLeave'
| 'contextMenu'
| 'mouseWheel'
| 'mouseMove'
| 'keyDown'
| 'keyUp'
| 'char';
keyCode: string;
}[];
};
export type NativefierOptions = Partial<
@ -81,9 +102,20 @@ export type NativefierOptions = Partial<
>;
export type OutputOptions = NativefierOptions & {
blockExternalUrls: boolean;
browserwindowOptions?: BrowserWindowOptions;
buildDate: number;
companyName?: string;
disableDevTools: boolean;
fileDownloadOptions?: Record<string, unknown>;
internalUrls: string | RegExp | undefined;
isUpgrade: boolean;
name: string;
nativefierVersion: string;
oldBuildWarningText: string;
targetUrl: string;
userAgent?: string;
zoom?: number;
};
export type PackageJSON = {
@ -120,7 +152,7 @@ export type RawOptions = {
electronVersionUsed?: string;
enableEs3Apis?: boolean;
fastQuit?: boolean;
fileDownloadOptions?: unknown;
fileDownloadOptions?: Record<string, unknown>;
flashPath?: string;
flashPluginDir?: string;
fullScreen?: boolean;
@ -150,7 +182,7 @@ export type RawOptions = {
showMenuBar?: boolean;
singleInstance?: boolean;
targetUrl?: string;
titleBarStyle?: string;
titleBarStyle?: TitleBarValue;
tray: TrayValue;
upgrade?: string | boolean;
upgradeFrom?: string;
@ -165,3 +197,25 @@ export type RawOptions = {
y?: number;
zoom?: number;
};
export type WindowOptions = {
blockExternalUrls: boolean;
browserwindowOptions?: BrowserWindowOptions;
insecure: boolean;
internalUrls?: string | RegExp;
name: string;
proxyRules?: string;
targetUrl: string;
userAgent?: string;
zoom: number;
};
export function outputOptionsToWindowOptions(
options: OutputOptions,
): WindowOptions {
return {
...options,
insecure: options.insecure ?? false,
zoom: options.zoom ?? 1.0,
};
}

18
shared/tsconfig.json Normal file
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,
convertToTrayIcon,
} from '../helpers/iconShellHelpers';
import { AppOptions } from '../options/model';
import { AppOptions } from '../../shared/src/options/model';
function iconIsIco(iconPath: string): boolean {
return path.extname(iconPath) === '.ico';

View File

@ -1,7 +1,7 @@
import * as path from 'path';
import * as electronGet from '@electron/get';
import * as electronPackager from 'electron-packager';
import electronPackager from 'electron-packager';
import * as log from 'loglevel';
import { convertIconIfNecessary } from './buildIcon';
@ -13,7 +13,7 @@ import {
isWindowsAdmin,
} from '../helpers/helpers';
import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade';
import { AppOptions, RawOptions } from '../options/model';
import { AppOptions, RawOptions } from '../../shared/src/options/model';
import { getOptions } from '../options/optionsMain';
import { prepareElectronApp } from './prepareElectronApp';

View File

@ -6,8 +6,13 @@ import { promisify } from 'util';
import * as log from 'loglevel';
import { copyFileOrDir, generateRandomSuffix } from '../helpers/helpers';
import { AppOptions, OutputOptions, PackageJSON } from '../options/model';
import {
AppOptions,
OutputOptions,
PackageJSON,
} from '../../shared/src/options/model';
import { parseJson } from '../utils/parseUtils';
import { DEFAULT_APP_NAME } from '../constants';
const writeFileAsync = promisify(fs.writeFile);
@ -66,7 +71,7 @@ function pickElectronAppArgs(options: AppOptions): OutputOptions {
maxWidth: options.nativefier.maxWidth,
minHeight: options.nativefier.minHeight,
minWidth: options.nativefier.minWidth,
name: options.packager.name,
name: options.packager.name ?? DEFAULT_APP_NAME,
nativefierVersion: options.nativefier.nativefierVersion,
osxNotarize: options.packager.osxNotarize,
osxSign: options.packager.osxSign,

View File

@ -3,7 +3,7 @@ import 'source-map-support/register';
import electronPackager = require('electron-packager');
import * as log from 'loglevel';
import * as yargs from 'yargs';
import yargs from 'yargs';
import { DEFAULT_ELECTRON_VERSION } from './constants';
import {
@ -13,7 +13,7 @@ import {
} from './helpers/helpers';
import { supportedArchs, supportedPlatforms } from './infer/inferOs';
import { buildNativefierApp } from './main';
import { RawOptions } from './options/model';
import { RawOptions } from '../shared/src/options/model';
import { parseJson } from './utils/parseUtils';
export function initArgs(argv: string[]): yargs.Argv<RawOptions> {

View File

@ -3,7 +3,7 @@ import * as path from 'path';
import * as log from 'loglevel';
import { NativefierOptions } from '../../options/model';
import { NativefierOptions } from '../../../shared/src/options/model';
import { getVersionString } from './rceditGet';
import { fileExists } from '../fsHelpers';
type ExecutableInfo = {

View File

@ -3,7 +3,10 @@ import * as path from 'path';
import * as log from 'loglevel';
import { NativefierOptions, RawOptions } from '../../options/model';
import {
NativefierOptions,
RawOptions,
} from '../../../shared/src/options/model';
import { dirExists, fileExists } from '../fsHelpers';
import { extractBoolean, extractString } from './plistInfoXMLHelpers';
import { getOptionsFromExecutable } from './executableHelpers';

View File

@ -3,7 +3,7 @@ import { writeFile } from 'fs';
import { promisify } from 'util';
import gitCloud = require('gitcloud');
import * as pageIcon from 'page-icon';
import pageIcon from 'page-icon';
import {
downloadFile,

View File

@ -10,7 +10,7 @@ import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion';
import { inferArch } from './infer/inferOs';
import { buildNativefierApp } from './main';
import { userAgent } from './options/fields/userAgent';
import { NativefierOptions, RawOptions } from './options/model';
import { NativefierOptions, RawOptions } from '../shared/src/options/model';
import { parseJson } from './utils/parseUtils';
async function checkApp(

View File

@ -1,7 +1,7 @@
import 'source-map-support/register';
import { buildNativefierApp } from './build/buildNativefierApp';
import { RawOptions } from './options/model';
import { RawOptions } from '../shared/src/options/model';
export { buildNativefierApp };

View File

@ -1,7 +1,7 @@
import * as log from 'loglevel';
import { processOptions } from './fields/fields';
import { AppOptions } from './model';
import { AppOptions } from '../../shared/src/options/model';
/**
* Takes the options object and infers new values needing async work

View File

@ -1,4 +1,4 @@
import { AppOptions } from '../model';
import { AppOptions } from '../../../shared/src/options/model';
import { processOptions } from './fields';
describe('fields', () => {
let options: AppOptions;

View File

@ -1,6 +1,6 @@
import { icon } from './icon';
import { userAgent } from './userAgent';
import { AppOptions } from '../model';
import { AppOptions } from '../../../shared/src/options/model';
import { name } from './name';
type OptionPostprocessor = {

View File

@ -1,7 +1,7 @@
import { getOptions, normalizePlatform } from './optionsMain';
import * as asyncConfig from './asyncConfig';
import { inferPlatform } from '../infer/inferOs';
import { AppOptions, RawOptions } from './model';
import { AppOptions, RawOptions } from '../../shared/src/options/model';
let asyncConfigMock: jest.SpyInstance;
const mockedAsyncConfig: AppOptions = {

View File

@ -19,7 +19,11 @@ import {
} from '../constants';
import { inferPlatform, inferArch } from '../infer/inferOs';
import { asyncConfig } from './asyncConfig';
import { AppOptions, GlobalShortcut, RawOptions } from './model';
import {
AppOptions,
GlobalShortcut,
RawOptions,
} from '../../shared/src/options/model';
import { normalizeUrl } from './normalizeUrl';
import { parseJson } from '../utils/parseUtils';

View File

@ -1,15 +1,8 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"allowJs": false,
"declaration": true,
"incremental": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./lib",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"outDir": "../lib",
"rootDir": ".",
// Bumping the minimum required Node version? You must bump:
// 1. package.json -> engines.node
// 2. package.json -> devDependencies.@types/node
@ -28,9 +21,11 @@
"lib": [
"es2020",
"dom"
]
],
},
"include": [
"./src/**/*"
"references": [
{
"path": "../shared"
}
]
}

14
tsconfig-base.json Normal file
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,
},
}