Enable TypeScript strict:true, and more typescript-eslint rules (#1223)

* Catch promise errors better

* Move subFunctions to bottom of createNewWindow

* Use parents when creating child BrowserWindow instances

* Some about:blank pages have an anchor (for some reason)

* Inject browserWindowOptions better

* Interim refactor to MainWindow object

* Split up the window functions/helpers/events some

* Further separate out window functions + tests

* Add a mock for unit testing functions that access electron

* Add unit tests for onWillPreventUnload

* Improve windowEvents tests

* Add the first test for windowHelpers

* Move WebRequest event handling to node

* insertCSS completely under test

* clearAppData completely under test

* Fix contextMenu require bug

* More tests + fixes

* Fix + add to createNewTab tests

* Convert createMainWindow back to func + work out gremlins

* Move setupWindow away from main since its shared

* Make sure contextMenu is handling promises

* Fix issues with fullscreen not working + menu refactor

* Run jest against app/dist so that we can hit app unit tests as well

* Requested PR changes

* Add strict typing; tests currently failing

* Fix failing unit tests

* Add some more eslint warnings and fixes

* More eslint fixes

* Strict typing on (still issues with the lib dir)

* Fix the package.json import/require

* Fix some funky test errors

* Warn -> Error for eslint rules

* @ts-ignore -> @ts-expect-error

* Add back the ext code I removed
This commit is contained in:
Adam Weeden 2021-06-15 22:20:49 -04:00 committed by GitHub
parent 7807bbb327
commit 7a08a2d676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 919 additions and 540 deletions

View File

@ -17,25 +17,29 @@ module.exports = {
rules: {
'no-console': 'error',
'prettier/prettier': [
'error', {
'endOfLine': 'auto'
}
'error',
{
endOfLine: 'auto',
},
],
// TODO remove when done killing `any`s and making tsc strict
'@typescript-eslint/ban-ts-comment': '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/explicit-function-return-type': 'error',
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-implicit-any-catch': 'error',
'@typescript-eslint/no-invalid-void-type': 'error',
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/typedef': 'error',
'@typescript-eslint/unified-signatures': 'error',
},
// https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files
ignorePatterns: [
"node_modules/**",
"app/node_modules/**",
"app/lib/**",
"lib/**",
"built-tests/**",
"coverage/**",
]
'node_modules/**',
'app/node_modules/**',
'app/lib/**',
'lib/**',
'built-tests/**',
'coverage/**',
],
};

2
.gitignore vendored
View File

@ -60,3 +60,5 @@ nativefier*.tgz
# https://github.com/nektos/act
.actrc
tsconfig.tsbuildinfo

View File

@ -25,14 +25,14 @@ export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
type SessionInteractionRequest = {
id?: string;
func?: string;
funcArgs?: any[];
funcArgs?: unknown[];
property?: string;
propertyValue?: any;
propertyValue?: unknown;
};
type SessionInteractionResult = {
id?: string;
value?: any;
value?: unknown;
error?: Error;
};
@ -144,20 +144,17 @@ function createContextMenu(options, window: BrowserWindow): void {
}
}
export function saveAppArgs(newAppArgs: any) {
export function saveAppArgs(newAppArgs: any): void {
try {
fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs));
} catch (err) {
// eslint-disable-next-line no-console
} catch (err: unknown) {
log.warn(
`WARNING: Ignored nativefier.json rewrital (${(
err as Error
).toString()})`,
`WARNING: Ignored nativefier.json rewrital (${(err as Error).message})`,
);
}
}
function setupCloseEvent(options, window: BrowserWindow) {
function setupCloseEvent(options, window: BrowserWindow): void {
window.on('close', (event: IpcMainEvent) => {
log.debug('mainWindow.close', event);
if (window.isFullScreen()) {
@ -181,7 +178,7 @@ function setupCounter(
options,
window: BrowserWindow,
setDockBadge: (value: number | string, bounce?: boolean) => void,
) {
): void {
window.on('page-title-updated', (event, title) => {
log.debug('mainWindow.page-title-updated', { event, title });
const counterValue = getCounterValue(title);
@ -242,7 +239,7 @@ function setupSessionInteraction(options, window: BrowserWindow): void {
typeof result.value['then'] === 'function'
) {
// This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply
result.value
(result.value as Promise<unknown>)
.then((trueResultValue) => {
result.value = trueResultValue;
log.debug('ipcMain.session-interaction:result', result);
@ -274,9 +271,9 @@ function setupSessionInteraction(options, window: BrowserWindow): void {
log.debug('session-interaction:result', result);
event.reply('session-interaction-reply', result);
}
} catch (error) {
log.error('session-interaction:error', error, event, request);
result.error = error;
} catch (err: unknown) {
log.error('session-interaction:error', err, event, request);
result.error = err as Error;
result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place
event.reply('session-interaction-reply', result);
}

View File

@ -89,9 +89,7 @@ export function generateMenu(
{
label: 'Copy Current URL',
accelerator: 'CmdOrCtrl+L',
click: () => {
clipboard.writeText(getCurrentURL());
},
click: (): void => clipboard.writeText(getCurrentURL()),
},
{
label: 'Paste',
@ -110,7 +108,7 @@ export function generateMenu(
},
{
label: 'Clear App Data',
click: (item: MenuItem, focusedWindow: BrowserWindow) => {
click: (item: MenuItem, focusedWindow: BrowserWindow): void => {
log.debug('Clear App Data.click', {
item,
focusedWindow,
@ -166,7 +164,7 @@ export function generateMenu(
accelerator: isOSX() ? 'Ctrl+Cmd+F' : 'F11',
enabled: mainWindow.isFullScreenable() || isOSX(),
visible: mainWindow.isFullScreenable() || isOSX(),
click: (item: MenuItem, focusedWindow: BrowserWindow) => {
click: (item: MenuItem, focusedWindow: BrowserWindow): void => {
log.debug('Toggle Full Screen.click()', {
item,
focusedWindow,
@ -266,9 +264,9 @@ export function generateMenu(
submenu: [
{
label: `Built with Nativefier v${nativefierVersion}`,
click: () => {
click: (): void => {
openExternal('https://github.com/nativefier/nativefier').catch(
(err) =>
(err: unknown): void =>
log.error(
'Built with Nativefier v${nativefierVersion}.click ERROR',
err,
@ -278,9 +276,10 @@ export function generateMenu(
},
{
label: 'Report an Issue',
click: () => {
click: (): void => {
openExternal('https://github.com/nativefier/nativefier/issues').catch(
(err) => log.error('Report an Issue.click ERROR', err),
(err: unknown): void =>
log.error('Report an Issue.click ERROR', err),
);
},
},
@ -370,8 +369,8 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
}
return {
label: bookmark.title,
click: () => {
goToURL(bookmark.url).catch((err) =>
click: (): void => {
goToURL(bookmark.url).catch((err: unknown): void =>
log.error(`${bookmark.title}.click ERROR`, err),
);
},
@ -390,7 +389,7 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
};
// Insert custom bookmarks menu between menus "View" and "Window"
menuTemplate.splice(menuTemplate.length - 2, 0, bookmarksMenu);
} catch (err) {
} catch (err: unknown) {
log.error('Failed to load & parse bookmarks configuration JSON file.', err);
}
}

View File

@ -23,7 +23,7 @@ export function createTrayIcon(
appIcon.setImage(nimage);
}
const onClick = () => {
const onClick = (): void => {
log.debug('onClick');
if (mainWindow.isVisible()) {
mainWindow.hide();

View File

@ -141,7 +141,7 @@ export function linkIsInternal(
// Only use the tld and the main domain for domain-ish test
// Enables domain-ish equality for blog.foo.com and shop.foo.com
return domainify(currentUrl) === domainify(newUrl);
} catch (err) {
} catch (err: unknown) {
log.error(
'Failed to parse domains as determining if link is internal. From:',
currentUrl,

View File

@ -4,6 +4,8 @@ import * as path from 'path';
import { isOSX, isWindows, isLinux } from './helpers';
type fsError = Error & { code: string };
/**
* Find a file or directory
*/
@ -14,12 +16,12 @@ function findSync(
): string[] {
const matches: string[] = [];
(function findSyncRecurse(base) {
(function findSyncRecurse(base): void {
let children: string[];
try {
children = fs.readdirSync(base);
} catch (err) {
if (err.code === 'ENOENT') {
} catch (err: unknown) {
if ((err as fsError).code === 'ENOENT') {
return;
}
throw err;
@ -51,18 +53,18 @@ function findSync(
return matches;
}
function findFlashOnLinux() {
function findFlashOnLinux(): string {
return findSync(/libpepflashplayer\.so/, '/opt/google/chrome')[0];
}
function findFlashOnWindows() {
function findFlashOnWindows(): string {
return findSync(
/pepflashplayer\.dll/,
'C:\\Program Files (x86)\\Google\\Chrome',
)[0];
}
function findFlashOnMac() {
function findFlashOnMac(): string {
return findSync(
/PepperFlashPlayer.plugin/,
'/Applications/Google Chrome.app/',
@ -70,7 +72,7 @@ function findFlashOnMac() {
)[0];
}
export function inferFlashPath() {
export function inferFlashPath(): string {
if (isOSX()) {
return findFlashOnMac();
}

View File

@ -1,4 +1,10 @@
import { dialog, BrowserWindow, IpcMainEvent, WebContents } from 'electron';
import {
dialog,
BrowserWindow,
IpcMainEvent,
NewWindowWebContentsEvent,
WebContents,
} from 'electron';
import log from 'loglevel';
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
@ -14,7 +20,7 @@ import {
export function onNewWindow(
options,
setupWindow: (...args) => void,
event: Event & { newGuest?: any },
event: NewWindowWebContentsEvent,
urlToGo: string,
frameName: string,
disposition:
@ -33,7 +39,7 @@ export function onNewWindow(
disposition,
parent,
});
const preventDefault = (newGuest: any): void => {
const preventDefault = (newGuest: BrowserWindow): void => {
event.preventDefault();
if (newGuest) {
event.newGuest = newGuest;
@ -96,7 +102,7 @@ export function onNewWindowHelper(
}
}
return Promise.resolve(undefined);
} catch (err) {
} catch (err: unknown) {
return Promise.reject(err);
}
}
@ -153,7 +159,7 @@ export function setupNativefierWindow(options, window: BrowserWindow): void {
window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => {
onWillNavigate(options, event, url).catch((err) => {
log.error(' window.webContents.on.will-navigate ERROR', err);
log.error('window.webContents.on.will-navigate ERROR', err);
event.preventDefault();
});
});
@ -161,14 +167,14 @@ export function setupNativefierWindow(options, window: BrowserWindow): void {
sendParamsOnDidFinishLoad(options, window);
// @ts-ignore new-tab isn't in the type definition, but it does exist
window.on('new-tab', () => {
// @ts-expect-error new-tab isn't in the type definition, but it does exist
window.on('new-tab', () =>
createNewTab(
options,
setupNativefierWindow,
options.targetUrl,
true,
window,
).catch((err) => log.error('new-tab ERROR', err));
});
),
);
}

View File

@ -140,7 +140,7 @@ describe('injectCSS', () => {
expect(mockGetCSSToInject).toHaveBeenCalled();
window.webContents.emit('did-navigate');
// @ts-ignore this function doesn't exist in the actual electron version, but will in our mock
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
window.webContents.session.webRequest.send(
'onHeadersReceived',
{ responseHeaders, webContents: window.webContents },
@ -167,7 +167,7 @@ describe('injectCSS', () => {
expect(mockGetCSSToInject).toHaveBeenCalled();
window.webContents.emit('did-navigate');
// @ts-ignore this function doesn't exist in the actual electron version, but will in our mock
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
window.webContents.session.webRequest.send(
'onHeadersReceived',
{ responseHeaders, webContents: window.webContents },
@ -202,7 +202,7 @@ describe('injectCSS', () => {
expect(window.webContents.emit('did-navigate')).toBe(true);
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-ignore this function doesn't exist in the actual electron version, but will in our mock
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
window.webContents.session.webRequest.send(
'onHeadersReceived',
{
@ -235,7 +235,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-ignore this function doesn't exist in the actual electron version, but will in our mock
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
window.webContents.session.webRequest.send(
'onHeadersReceived',
{
@ -270,7 +270,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-ignore this function doesn't exist in the actual electron version, but will in our mock
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
window.webContents.session.webRequest.send(
'onHeadersReceived',
{
@ -302,7 +302,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-ignore this function doesn't exist in the actual electron version, but will in our mock
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
window.webContents.session.webRequest.send(
'onHeadersReceived',
{

View File

@ -83,7 +83,7 @@ export function createNewTab(
url: string,
foreground: boolean,
parent?: BrowserWindow,
): Promise<BrowserWindow> {
): BrowserWindow {
log.debug('createNewTab', { url, foreground, parent });
return withFocusedWindow((focusedWindow) => {
const newTab = createNewWindow(options, setupWindow, url, parent);
@ -314,7 +314,7 @@ export function setProxyRules(window: BrowserWindow, proxyRules): void {
.catch((err) => log.error('session.setProxy ERROR', err));
}
export function withFocusedWindow(block: (window: BrowserWindow) => any): any {
export function withFocusedWindow<T>(block: (window: BrowserWindow) => T): T {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
return block(focusedWindow);

View File

@ -3,7 +3,7 @@ import 'source-map-support/register';
import fs from 'fs';
import * as path from 'path';
import {
import electron, {
app,
crashReporter,
dialog,
@ -67,10 +67,11 @@ if (process.argv.length > 1) {
new URL(maybeUrl);
appArgs.targetUrl = maybeUrl;
log.info('Loading override URL passed as argument:', maybeUrl);
} catch (err) {
} catch (err: unknown) {
log.error(
'Not loading override URL passed as argument, because failed to parse:',
maybeUrl,
err,
);
}
}
@ -155,12 +156,12 @@ if (appArgs.lang) {
let currentBadgeCount = 0;
const setDockBadge = isOSX()
? (count: number, bounce = false) => {
? (count: number, bounce = false): void => {
app.dock.setBadge(count.toString());
if (bounce && count > currentBadgeCount) app.dock.bounce();
currentBadgeCount = count;
}
: () => undefined;
: (): void => undefined;
app.on('window-all-closed', () => {
log.debug('app.window-all-closed');
@ -203,14 +204,14 @@ if (appArgs.crashReporter) {
}
if (appArgs.widevine) {
// @ts-ignore This event only appears on the widevine version of electron, which we'd see at runtime
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
app.on('widevine-ready', (version: string, lastVersion: string) => {
log.debug('app.widevine-ready', { version, lastVersion });
onReady().catch((err) => log.error('onReady ERROR', err));
});
app.on(
// @ts-ignore This event only appears on the widevine version of electron, which we'd see at runtime
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
'widevine-update-pending',
(currentVersion: string, pendingVersion: string) => {
log.debug('app.widevine-update-pending', {
@ -220,8 +221,8 @@ if (appArgs.widevine) {
},
);
// @ts-ignore This event only appears on the widevine version of electron, which we'd see at runtime
app.on('widevine-error', (error: any) => {
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
app.on('widevine-error', (error: Error) => {
log.error('app.widevine-error', error);
});
} else {
@ -231,7 +232,7 @@ if (appArgs.widevine) {
});
}
app.on('activate', (event, hasVisibleWindows) => {
app.on('activate', (event: electron.Event, hasVisibleWindows: boolean) => {
log.debug('app.activate', { event, hasVisibleWindows });
if (isOSX()) {
// this is called when the dock is clicked
@ -374,7 +375,7 @@ app.on(
app.on(
'activity-was-continued',
(event: IpcMainEvent, type: string, userInfo: any) => {
(event: IpcMainEvent, type: string, userInfo: unknown) => {
log.debug('app.activity-was-continued', { event, type, userInfo });
},
);

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { EventEmitter } from 'events';
@ -21,12 +22,12 @@ import { EventEmitter } from 'events';
class MockBrowserWindow extends EventEmitter {
webContents: MockWebContents;
constructor(options?: any) {
constructor(options?: unknown) {
super(options);
this.webContents = new MockWebContents();
}
addTabbedWindow(tab: MockBrowserWindow) {
addTabbedWindow(tab: MockBrowserWindow): void {
return;
}
@ -54,7 +55,7 @@ class MockBrowserWindow extends EventEmitter {
return undefined;
}
loadURL(url: string, options?: any): Promise<void> {
loadURL(url: string, options?: unknown): Promise<void> {
return Promise.resolve(undefined);
}
@ -70,14 +71,14 @@ class MockBrowserWindow extends EventEmitter {
class MockDialog {
static showMessageBox(
browserWindow: MockBrowserWindow,
options: any,
options: unknown,
): Promise<number> {
return Promise.resolve(undefined);
}
static showMessageBoxSync(
browserWindow: MockBrowserWindow,
options: any,
options: unknown,
): number {
return undefined;
}
@ -112,7 +113,7 @@ class MockWebContents extends EventEmitter {
return undefined;
}
insertCSS(css: string, options?: any): Promise<string> {
insertCSS(css: string, options?: unknown): Promise<string> {
return Promise.resolve(undefined);
}
}
@ -125,22 +126,24 @@ class MockWebRequest {
}
onHeadersReceived(
filter: any,
filter: unknown,
listener:
| ((
details: any,
callback: (headersReceivedResponse: any) => void,
details: unknown,
callback: (headersReceivedResponse: unknown) => void,
) => void)
| null,
): void {
this.emitter.addListener(
'onHeadersReceived',
(details: any, callback: (headersReceivedResponse: any) => void) =>
listener(details, callback),
(
details: unknown,
callback: (headersReceivedResponse: unknown) => void,
) => listener(details, callback),
);
}
send(event: string, ...args: any[]): void {
send(event: string, ...args: unknown[]): void {
this.emitter.emit(event, ...args);
}
}

View File

@ -34,9 +34,18 @@ export const INJECT_DIR = path.join(__dirname, '..', 'inject');
* @param createCallback
* @param clickCallback
*/
function setNotificationCallback(createCallback, clickCallback) {
function setNotificationCallback(
createCallback: {
(title: string, opt: NotificationOptions): void;
(...args: unknown[]): void;
},
clickCallback: { (): void; (this: Notification, ev: Event): unknown },
): void {
const OldNotify = window.Notification;
const newNotify = function (title, opt) {
const newNotify = function (
title: string,
opt: NotificationOptions,
): Notification {
createCallback(title, opt);
const instance = new OldNotify(title, opt);
instance.addEventListener('click', clickCallback);
@ -47,11 +56,11 @@ function setNotificationCallback(createCallback, clickCallback) {
get: () => OldNotify.permission,
});
// @ts-ignore
// @ts-expect-error
window.Notification = newNotify;
}
function injectScripts() {
function injectScripts(): void {
const needToInject = fs.existsSync(INJECT_DIR);
if (!needToInject) {
return;
@ -68,15 +77,18 @@ function injectScripts() {
log.debug('Injecting JS file', jsFile);
require(jsFile);
}
} catch (error) {
log.error('Error encoutered injecting JS files', error);
} catch (err: unknown) {
log.error('Error encoutered injecting JS files', err);
}
}
function notifyNotificationCreate(title, opt) {
function notifyNotificationCreate(
title: string,
opt: NotificationOptions,
): void {
ipcRenderer.send('notification', title, opt);
}
function notifyNotificationClick() {
function notifyNotificationClick(): void {
ipcRenderer.send('notification-click');
}

View File

@ -37,8 +37,8 @@
"build:watch": "npm run clean && tsc --build . app --watch",
"changelog": "./.github/generate-changelog",
"ci": "npm run lint && npm test",
"clean": "rimraf lib/ app/lib/ app/dist/",
"clean:full": "rimraf lib/ app/lib/ app/dist/ app/node_modules/ node_modules/",
"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",
@ -66,7 +66,10 @@
"yargs": "^17.0.1"
},
"devDependencies": {
"@types/electron-packager": "^15.0.1",
"@types/hasbin": "^1.2.0",
"@types/jest": "^26.0.23",
"@types/loglevel": "^1.6.3",
"@types/ncp": "^2.0.4",
"@types/node": "^12.20.15",
"@types/page-icon": "^0.3.3",

View File

@ -45,8 +45,8 @@ export function convertIconIfNecessary(options: AppOptions): void {
const iconPath = convertToIco(options.packager.icon);
options.packager.icon = iconPath;
return;
} catch (error) {
log.warn('Failed to convert icon to .ico, skipping.', error);
} catch (err: unknown) {
log.warn('Failed to convert icon to .ico, skipping.', err);
return;
}
}
@ -63,8 +63,8 @@ export function convertIconIfNecessary(options: AppOptions): void {
const iconPath = convertToPng(options.packager.icon);
options.packager.icon = iconPath;
return;
} catch (error) {
log.warn('Failed to convert icon to .png, skipping.', error);
} catch (err: unknown) {
log.warn('Failed to convert icon to .png, skipping.', err);
return;
}
}
@ -90,8 +90,8 @@ export function convertIconIfNecessary(options: AppOptions): void {
if (options.nativefier.tray) {
convertToTrayIcon(options.packager.icon);
}
} catch (error) {
log.warn('Failed to convert icon to .icns, skipping.', error);
} catch (err: unknown) {
log.warn('Failed to convert icon to .icns, skipping.', err);
options.packager.icon = undefined;
}
}

View File

@ -2,18 +2,18 @@ import * as path from 'path';
import * as electronGet from '@electron/get';
import * as electronPackager from 'electron-packager';
import * as hasbin from 'hasbin';
import * as log from 'loglevel';
import { convertIconIfNecessary } from './buildIcon';
import {
copyFileOrDir,
getTempDir,
hasWine,
isWindows,
isWindowsAdmin,
} from '../helpers/helpers';
import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade';
import { AppOptions } from '../options/model';
import { AppOptions, RawOptions } from '../options/model';
import { getOptions } from '../options/optionsMain';
import { prepareElectronApp } from './prepareElectronApp';
@ -70,13 +70,13 @@ async function copyIconsIfNecessary(
/**
* Checks the app path array to determine if packaging completed successfully
*/
function getAppPath(appPath: string | string[]): string {
function getAppPath(appPath: string | string[]): string | undefined {
if (!Array.isArray(appPath)) {
return appPath;
}
if (appPath.length === 0) {
return null; // directory already exists and `--overwrite` not set
return undefined; // directory already exists and `--overwrite` not set
}
if (appPath.length > 1) {
@ -89,20 +89,21 @@ function getAppPath(appPath: string | string[]): string {
return appPath[0];
}
function isUpgrade(rawOptions) {
return (
function isUpgrade(rawOptions: RawOptions): boolean {
if (
rawOptions.upgrade !== undefined &&
(rawOptions.upgrade === true ||
(typeof rawOptions.upgrade === 'string' && rawOptions.upgrade !== ''))
);
typeof rawOptions.upgrade === 'string' &&
rawOptions.upgrade !== ''
) {
rawOptions.upgradeFrom = rawOptions.upgrade;
rawOptions.upgrade = true;
return true;
}
return false;
}
function trimUnprocessableOptions(options: AppOptions): void {
if (
options.packager.platform === 'win32' &&
!isWindows() &&
!hasbin.sync('wine')
) {
if (options.packager.platform === 'win32' && !isWindows() && !hasWine()) {
const optionsPresent = Object.entries(options)
.filter(
([key, value]) =>
@ -119,28 +120,30 @@ function trimUnprocessableOptions(options: AppOptions): void {
'features, like a correct icon and process name. Do yourself a favor and install Wine, please.',
);
for (const keyToUnset of optionsPresent) {
options[keyToUnset] = null;
(options as unknown as Record<string, undefined>)[keyToUnset] = undefined;
}
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function buildNativefierApp(rawOptions): Promise<string> {
export async function buildNativefierApp(
rawOptions: RawOptions,
): Promise<string | undefined> {
log.info('\nProcessing options...');
if (isUpgrade(rawOptions)) {
log.debug('Attempting to upgrade from', rawOptions.upgrade);
const oldApp = findUpgradeApp(rawOptions.upgrade.toString());
log.debug('Attempting to upgrade from', rawOptions.upgradeFrom);
const oldApp = findUpgradeApp(rawOptions.upgradeFrom as string);
if (oldApp === null) {
throw new Error(
`Could not find an old Nativfier app in "${
rawOptions.upgrade as string
rawOptions.upgradeFrom as string
}"`,
);
}
rawOptions = useOldAppOptions(rawOptions, oldApp);
if (rawOptions.out === undefined && rawOptions.overwrite) {
rawOptions.out = path.dirname(rawOptions.upgrade);
rawOptions.out = path.dirname(rawOptions.upgradeFrom as string);
}
}
log.debug('rawOptions', rawOptions);

View File

@ -6,14 +6,15 @@ import { promisify } from 'util';
import * as log from 'loglevel';
import { copyFileOrDir, generateRandomSuffix } from '../helpers/helpers';
import { AppOptions } from '../options/model';
import { AppOptions, OutputOptions, PackageJSON } from '../options/model';
import { parseJson } from '../utils/parseUtils';
const writeFileAsync = promisify(fs.writeFile);
/**
* Only picks certain app args to pass to nativefier.json
*/
function pickElectronAppArgs(options: AppOptions): any {
function pickElectronAppArgs(options: AppOptions): OutputOptions {
return {
accessibilityPrompt: options.nativefier.accessibilityPrompt,
alwaysOnTop: options.nativefier.alwaysOnTop,
@ -99,7 +100,10 @@ function pickElectronAppArgs(options: AppOptions): any {
};
}
async function maybeCopyScripts(srcs: string[], dest: string): Promise<void> {
async function maybeCopyScripts(
srcs: string[] | undefined,
dest: string,
): Promise<void> {
if (!srcs || srcs.length === 0) {
log.debug('No files to inject, skipping copy.');
return;
@ -151,7 +155,12 @@ function changeAppPackageJsonName(
url: string,
): void {
const packageJsonPath = path.join(appPath, '/package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString());
const packageJson = parseJson<PackageJSON>(
fs.readFileSync(packageJsonPath).toString(),
);
if (!packageJson) {
throw new Error(`Could not load package.json from ${packageJsonPath}`);
}
const normalizedAppName = normalizeAppName(name, url);
packageJson.name = normalizedAppName;
log.debug(`Updating ${packageJsonPath} 'name' field to ${normalizedAppName}`);
@ -171,10 +180,10 @@ export async function prepareElectronApp(
log.debug(`Copying electron app from ${src} to ${dest}`);
try {
await copyFileOrDir(src, dest);
} catch (err) {
throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${(
err as Error
).toString()}`;
} catch (err: unknown) {
throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${
(err as Error).message
}`;
}
const appJsonPath = path.join(dest, '/nativefier.json');
@ -188,19 +197,19 @@ export async function prepareElectronApp(
const bookmarksJsonPath = path.join(dest, '/bookmarks.json');
try {
await copyFileOrDir(options.nativefier.bookmarksMenu, bookmarksJsonPath);
} catch (err) {
} catch (err: unknown) {
log.error('Error copying bookmarks menu config file.', err);
}
}
try {
await maybeCopyScripts(options.nativefier.inject, dest);
} catch (err) {
} catch (err: unknown) {
log.error('Error copying injection files.', err);
}
changeAppPackageJsonName(
dest,
options.packager.name,
options.packager.name as string,
options.packager.targetUrl,
);
}

View File

@ -51,22 +51,22 @@ describe('initArgs + parseArgs', () => {
test('upgrade arg', () => {
const args = parseArgs(initArgs(['--upgrade', 'pathToUpgrade']));
expect(args.upgrade).toBe('pathToUpgrade');
expect(args.targetUrl).toBe('');
expect(args.targetUrl).toBeUndefined();
});
test('upgrade arg with out dir', () => {
const args = parseArgs(initArgs(['tmp', '--upgrade', 'pathToUpgrade']));
expect(args.upgrade).toBe('pathToUpgrade');
expect(args.out).toBe('tmp');
expect(args.targetUrl).toBe('');
expect(args.targetUrl).toBeUndefined();
});
test('upgrade arg with targetUrl', () => {
expect(() => {
expect(() =>
parseArgs(
initArgs(['https://www.google.com', '--upgrade', 'path/to/upgrade']),
);
}).toThrow();
),
).toThrow();
});
test('multi-inject', () => {
@ -92,53 +92,55 @@ describe('initArgs + parseArgs', () => {
});
test.each([
{ arg: 'app-copyright', shortArg: null, value: '(c) Nativefier' },
{ arg: 'app-version', shortArg: null, value: '2.0.0' },
{ arg: 'background-color', shortArg: null, value: '#FFAA88' },
{ arg: 'basic-auth-username', shortArg: null, value: 'user' },
{ arg: 'basic-auth-password', shortArg: null, value: 'p@ssw0rd' },
{ arg: 'bookmarks-menu', shortArg: null, value: 'bookmarks.json' },
{ arg: 'app-copyright', shortArg: '', value: '(c) Nativefier' },
{ arg: 'app-version', shortArg: '', value: '2.0.0' },
{ arg: 'background-color', shortArg: '', value: '#FFAA88' },
{ arg: 'basic-auth-username', shortArg: '', value: 'user' },
{ arg: 'basic-auth-password', shortArg: '', value: 'p@ssw0rd' },
{ arg: 'bookmarks-menu', shortArg: '', value: 'bookmarks.json' },
{
arg: 'browserwindow-options',
shortArg: null,
shortArg: '',
value: '{"test": 456}',
isJsonString: true,
},
{ arg: 'build-version', shortArg: null, value: '3.0.0' },
{ arg: 'build-version', shortArg: '', value: '3.0.0' },
{
arg: 'crash-reporter',
shortArg: null,
shortArg: '',
value: 'https://crash-reporter.com',
},
{ arg: 'electron-version', shortArg: 'e', value: '1.0.0' },
{
arg: 'file-download-options',
shortArg: null,
shortArg: '',
value: '{"test": 789}',
isJsonString: true,
},
{ arg: 'flash-path', shortArg: null, value: 'pathToFlash' },
{ arg: 'global-shortcuts', shortArg: null, value: 'shortcuts.json' },
{ arg: 'flash-path', shortArg: '', value: 'pathToFlash' },
{ arg: 'global-shortcuts', shortArg: '', value: 'shortcuts.json' },
{ arg: 'icon', shortArg: 'i', value: 'icon.png' },
{ arg: 'internal-urls', shortArg: null, value: '.*' },
{ arg: 'lang', shortArg: null, value: 'fr' },
{ arg: 'internal-urls', shortArg: '', value: '.*' },
{ arg: 'lang', shortArg: '', value: 'fr' },
{ arg: 'name', shortArg: 'n', value: 'Google' },
{
arg: 'process-envs',
shortArg: null,
shortArg: '',
value: '{"test": 123}',
isJsonString: true,
},
{ arg: 'proxy-rules', shortArg: null, value: 'RULE: PROXY' },
{ arg: 'proxy-rules', shortArg: '', value: 'RULE: PROXY' },
{ arg: 'user-agent', shortArg: 'u', value: 'FIREFOX' },
{
arg: 'win32metadata',
shortArg: null,
shortArg: '',
value: '{"ProductName": "Google"}',
isJsonString: true,
},
])('test string arg %s', ({ arg, shortArg, value, isJsonString }) => {
const args = parseArgs(initArgs(['https://google.com', `--${arg}`, value]));
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`, value]),
) as Record<string, string>;
if (!isJsonString) {
expect(args[arg]).toBe(value);
} else {
@ -147,8 +149,8 @@ describe('initArgs + parseArgs', () => {
if (shortArg) {
const argsShort = parseArgs(
initArgs(['https://google.com', `-${shortArg as string}`, value]),
);
initArgs(['https://google.com', `-${shortArg}`, value]),
) as Record<string, string>;
if (!isJsonString) {
expect(argsShort[arg]).toBe(value);
} else {
@ -162,12 +164,14 @@ describe('initArgs + parseArgs', () => {
{ arg: 'platform', shortArg: 'p', value: 'mac', badValue: 'os2' },
{
arg: 'title-bar-style',
shortArg: null,
shortArg: '',
value: 'hidden',
badValue: 'cool',
},
])('limited choice arg %s', ({ arg, shortArg, value, badValue }) => {
const args = parseArgs(initArgs(['https://google.com', `--${arg}`, value]));
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`, value]),
) as Record<string, string>;
expect(args[arg]).toBe(value);
// Mock console.error to not pollute the log with the yargs help text
@ -181,7 +185,7 @@ describe('initArgs + parseArgs', () => {
if (shortArg) {
const argsShort = parseArgs(
initArgs(['https://google.com', `-${shortArg}`, value]),
);
) as Record<string, string>;
expect(argsShort[arg]).toBe(value);
initArgs(['https://google.com', `-${shortArg}`, badValue]);
@ -192,64 +196,74 @@ describe('initArgs + parseArgs', () => {
});
test.each([
{ arg: 'always-on-top', shortArg: null },
{ arg: 'block-external-urls', shortArg: null },
{ arg: 'bounce', shortArg: null },
{ arg: 'clear-cache', shortArg: null },
{ arg: 'always-on-top', shortArg: '' },
{ arg: 'block-external-urls', shortArg: '' },
{ arg: 'bounce', shortArg: '' },
{ arg: 'clear-cache', shortArg: '' },
{ arg: 'conceal', shortArg: 'c' },
{ arg: 'counter', shortArg: null },
{ arg: 'darwin-dark-mode-support', shortArg: null },
{ arg: 'disable-context-menu', shortArg: null },
{ arg: 'disable-dev-tools', shortArg: null },
{ arg: 'disable-gpu', shortArg: null },
{ arg: 'disable-old-build-warning-yesiknowitisinsecure', shortArg: null },
{ arg: 'enable-es3-apis', shortArg: null },
{ arg: 'counter', shortArg: '' },
{ arg: 'darwin-dark-mode-support', shortArg: '' },
{ arg: 'disable-context-menu', shortArg: '' },
{ arg: 'disable-dev-tools', shortArg: '' },
{ arg: 'disable-gpu', shortArg: '' },
{ arg: 'disable-old-build-warning-yesiknowitisinsecure', shortArg: '' },
{ arg: 'enable-es3-apis', shortArg: '' },
{ arg: 'fast-quit', shortArg: 'f' },
{ arg: 'flash', shortArg: null },
{ arg: 'full-screen', shortArg: null },
{ arg: 'hide-window-frame', shortArg: null },
{ arg: 'honest', shortArg: null },
{ arg: 'ignore-certificate', shortArg: null },
{ arg: 'ignore-gpu-blacklist', shortArg: null },
{ arg: 'insecure', shortArg: null },
{ arg: 'maximize', shortArg: null },
{ arg: 'portable', shortArg: null },
{ arg: 'flash', shortArg: '' },
{ arg: 'full-screen', shortArg: '' },
{ arg: 'hide-window-frame', shortArg: '' },
{ arg: 'honest', shortArg: '' },
{ arg: 'ignore-certificate', shortArg: '' },
{ arg: 'ignore-gpu-blacklist', shortArg: '' },
{ arg: 'insecure', shortArg: '' },
{ arg: 'maximize', shortArg: '' },
{ arg: 'portable', shortArg: '' },
{ arg: 'show-menu-bar', shortArg: 'm' },
{ arg: 'single-instance', shortArg: null },
{ arg: 'tray', shortArg: null },
{ arg: 'verbose', shortArg: null },
{ arg: 'widevine', shortArg: null },
{ arg: 'single-instance', shortArg: '' },
{ arg: 'tray', shortArg: '' },
{ arg: 'verbose', shortArg: '' },
{ arg: 'widevine', shortArg: '' },
])('test boolean arg %s', ({ arg, shortArg }) => {
const defaultArgs = parseArgs(initArgs(['https://google.com']));
const defaultArgs = parseArgs(initArgs(['https://google.com'])) as Record<
string,
boolean
>;
expect(defaultArgs[arg]).toBe(false);
const args = parseArgs(initArgs(['https://google.com', `--${arg}`]));
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`]),
) as Record<string, boolean>;
expect(args[arg]).toBe(true);
if (shortArg) {
const argsShort = parseArgs(
initArgs(['https://google.com', `-${shortArg}`]),
);
) as Record<string, boolean>;
expect(argsShort[arg]).toBe(true);
}
});
test.each([{ arg: 'no-overwrite', shortArg: null }])(
test.each([{ arg: 'no-overwrite', shortArg: '' }])(
'test inversible boolean arg %s',
({ arg, shortArg }) => {
const inverse = arg.startsWith('no-') ? arg.substr(3) : `no-${arg}`;
const defaultArgs = parseArgs(initArgs(['https://google.com']));
const defaultArgs = parseArgs(initArgs(['https://google.com'])) as Record<
string,
boolean
>;
expect(defaultArgs[arg]).toBe(false);
expect(defaultArgs[inverse]).toBe(true);
const args = parseArgs(initArgs(['https://google.com', `--${arg}`]));
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`]),
) as Record<string, boolean>;
expect(args[arg]).toBe(true);
expect(args[inverse]).toBe(false);
if (shortArg) {
const argsShort = parseArgs(
initArgs(['https://google.com', `-${shortArg as string}`]),
);
initArgs(['https://google.com', `-${shortArg}`]),
) as Record<string, boolean>;
expect(argsShort[arg]).toBe(true);
expect(argsShort[inverse]).toBe(true);
}
@ -257,35 +271,35 @@ describe('initArgs + parseArgs', () => {
);
test.each([
{ arg: 'disk-cache-size', shortArg: null, value: 100 },
{ arg: 'height', shortArg: null, value: 200 },
{ arg: 'max-height', shortArg: null, value: 300 },
{ arg: 'max-width', shortArg: null, value: 400 },
{ arg: 'min-height', shortArg: null, value: 500 },
{ arg: 'min-width', shortArg: null, value: 600 },
{ arg: 'width', shortArg: null, value: 700 },
{ arg: 'x', shortArg: null, value: 800 },
{ arg: 'y', shortArg: null, value: 900 },
{ arg: 'disk-cache-size', shortArg: '', value: 100 },
{ arg: 'height', shortArg: '', value: 200 },
{ arg: 'max-height', shortArg: '', value: 300 },
{ arg: 'max-width', shortArg: '', value: 400 },
{ arg: 'min-height', shortArg: '', value: 500 },
{ arg: 'min-width', shortArg: '', value: 600 },
{ arg: 'width', shortArg: '', value: 700 },
{ arg: 'x', shortArg: '', value: 800 },
{ arg: 'y', shortArg: '', value: 900 },
])('test numeric arg %s', ({ arg, shortArg, value }) => {
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`, `${value}`]),
);
) as Record<string, number>;
expect(args[arg]).toBe(value);
const badArgs = parseArgs(
initArgs(['https://google.com', `--${arg}`, 'abcd']),
);
) as Record<string, number>;
expect(badArgs[arg]).toBeNaN();
if (shortArg) {
const shortArgs = parseArgs(
initArgs(['https://google.com', `-${shortArg as string}`, `${value}`]),
);
initArgs(['https://google.com', `-${shortArg}`, `${value}`]),
) as Record<string, number>;
expect(shortArgs[arg]).toBe(value);
const badShortArgs = parseArgs(
initArgs(['https://google.com', `-${shortArg as string}`, 'abcd']),
);
initArgs(['https://google.com', `-${shortArg}`, 'abcd']),
) as Record<string, number>;
expect(badShortArgs[arg]).toBeNaN();
}
});

View File

@ -11,11 +11,12 @@ import {
} from './helpers/helpers';
import { supportedArchs, supportedPlatforms } from './infer/inferOs';
import { buildNativefierApp } from './main';
import { NativefierOptions } from './options/model';
import { RawOptions } from './options/model';
import { parseJson } from './utils/parseUtils';
import { DEFAULT_ELECTRON_VERSION } from './constants';
import electronPackager = require('electron-packager');
export function initArgs(argv: string[]): yargs.Argv<any> {
export function initArgs(argv: string[]): yargs.Argv<RawOptions> {
const args = yargs(argv)
.scriptName('nativefier')
.usage(
@ -160,7 +161,6 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
coerce: parseJson,
description:
'override Electron BrowserWindow options (via JSON string); see https://github.com/nativefier/nativefier/blob/master/API.md#browserwindow-options',
type: 'string',
})
.option('disable-context-menu', {
default: false,
@ -222,7 +222,6 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
coerce: getProcessEnvs,
description:
'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened',
type: 'string',
})
.option('single-instance', {
default: false,
@ -285,11 +284,11 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
coerce: parseJson,
description:
'a JSON string defining file download options; see https://github.com/sindresorhus/electron-dl',
type: 'string',
})
.option('inject', {
description:
'path to a CSS/JS file to be injected; pass multiple times to inject multiple files',
string: true,
type: 'array',
})
.option('lang', {
@ -477,10 +476,10 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
type: 'string',
})
.option('win32metadata', {
coerce: parseJson,
coerce: (value: string) =>
parseJson<electronPackager.Win32MetadataOptions>(value),
description:
'(windows only) a JSON string of key/value pairs (ProductName, InternalName, FileDescription) to embed as executable metadata',
type: 'string',
})
.group(
[
@ -526,17 +525,17 @@ function decorateYargOptionGroup(value: string): string {
return `====== ${value} ======`;
}
export function parseArgs(args: yargs.Argv<any>): any {
export function parseArgs(args: yargs.Argv<RawOptions>): RawOptions {
const parsed = { ...args.argv };
// In yargs, the _ property of the parsed args is an array of the positional args
// https://github.com/yargs/yargs/blob/master/docs/examples.md#and-non-hyphenated-options-too-just-use-argv_
// So try to extract the targetUrl and outputDirectory from these
parsed.targetUrl = parsed._.length > 0 ? parsed._[0].toString() : '';
parsed.out = parsed._.length > 1 ? parsed._[1] : '';
parsed.targetUrl = parsed._.length > 0 ? parsed._[0].toString() : undefined;
parsed.out = parsed._.length > 1 ? (parsed._[1] as string) : undefined;
if (parsed.upgrade && parsed.targetUrl !== '') {
if (parsed.upgrade && parsed.targetUrl) {
let targetAndUpgrade = false;
if (parsed.out === '') {
if (!parsed.out) {
// If we're upgrading, the first positional args might be the outputDirectory, so swap these if we can
try {
// If this succeeds, we have a problem
@ -545,7 +544,7 @@ export function parseArgs(args: yargs.Argv<any>): any {
} catch {
// Cool, it's not a URL
parsed.out = parsed.targetUrl;
parsed.targetUrl = '';
parsed.targetUrl = undefined;
}
} else {
// Someone supplied a targetUrl, an outputDirectory, and --upgrade. That's not cool.
@ -559,7 +558,7 @@ export function parseArgs(args: yargs.Argv<any>): any {
}
}
if (parsed.targetUrl === '' && !parsed.upgrade) {
if (!parsed.targetUrl && !parsed.upgrade) {
throw new Error(
'ERROR: Nativefier must be called with either a targetUrl or the --upgrade option.\n',
);
@ -572,7 +571,7 @@ export function parseArgs(args: yargs.Argv<any>): any {
if (require.main === module) {
// Not sure if we still need this with yargs. Keeping for now.
const sanitizedArgs = [];
const sanitizedArgs: string[] = [];
process.argv.forEach((arg) => {
if (isArgFormatInvalid(arg)) {
throw new Error(
@ -593,11 +592,12 @@ if (require.main === module) {
sanitizedArgs.push(arg);
});
let args, parsedArgs;
let args: yargs.Argv<RawOptions> | undefined = undefined;
let parsedArgs: RawOptions;
try {
args = initArgs(sanitizedArgs.slice(2));
parsedArgs = parseArgs(args);
} catch (err) {
} catch (err: unknown) {
if (args) {
log.error(err);
args.showHelp();
@ -607,7 +607,7 @@ if (require.main === module) {
process.exit(1);
}
const options: NativefierOptions = {
const options: RawOptions = {
...parsedArgs,
};

View File

@ -17,11 +17,17 @@ tmp.setGracefulCleanup(); // cleanup temp dirs even when an uncaught exception o
const now = new Date();
const TMP_TIME = `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`;
type DownloadResult = {
export type DownloadResult = {
data: Buffer;
ext: string;
};
type ProcessEnvs = Record<string, unknown>;
export function hasWine(): boolean {
return hasbin.sync('wine');
}
export function isOSX(): boolean {
return os.platform() === 'darwin';
}
@ -57,7 +63,7 @@ export async function copyFileOrDir(
dest: string,
): Promise<void> {
return new Promise((resolve, reject) => {
ncp(sourceFileOrDir, dest, (error: any) => {
ncp(sourceFileOrDir, dest, (error: Error[] | null): void => {
if (error) {
reject(error);
}
@ -66,15 +72,17 @@ export async function copyFileOrDir(
});
}
export function downloadFile(fileUrl: string): Promise<DownloadResult> {
export function downloadFile(
fileUrl: string,
): Promise<DownloadResult | undefined> {
log.debug(`Downloading ${fileUrl}`);
return axios
.get(fileUrl, {
.get<Buffer>(fileUrl, {
responseType: 'arraybuffer',
})
.then((response) => {
if (!response.data) {
return null;
return undefined;
}
return {
data: response.data,
@ -97,7 +105,7 @@ export function getAllowedIconFormats(platform: string): string[] {
const icnsToPng = false;
const icnsToIco = false;
const formats = [];
const formats: string[] = [];
// Shell scripting is not supported on windows, temporary override
if (isWindows()) {
@ -175,11 +183,11 @@ export function generateRandomSuffix(length = 6): string {
return hash.digest('hex').substring(0, length);
}
export function getProcessEnvs(val: string): any {
export function getProcessEnvs(val: string): ProcessEnvs | undefined {
if (!val) {
return {};
return undefined;
}
return parseJson(val);
return parseJson<ProcessEnvs>(val);
}
export function checkInternet(): void {

View File

@ -65,9 +65,9 @@ function getExecutableArch(
function getExecutableInfo(
executablePath: string,
platform: string,
): ExecutableInfo {
): ExecutableInfo | undefined {
if (!fileExists(executablePath)) {
return {};
return undefined;
}
const exeBytes = getExecutableBytes(executablePath);
@ -81,6 +81,12 @@ export function getOptionsFromExecutable(
priorOptions: NativefierOptions,
): NativefierOptions {
const newOptions: NativefierOptions = { ...priorOptions };
if (!newOptions.name) {
throw new Error(
'Can not extract options from executable with no name specified.',
);
}
const name: string = newOptions.name;
let executablePath: string | undefined = undefined;
const appRoot = path.resolve(path.join(appResourcesDir, '..', '..'));
@ -118,8 +124,7 @@ export function getOptionsFromExecutable(
appRoot,
children.filter(
(c) =>
c.name.toLowerCase() === `${newOptions.name.toLowerCase()}.exe` &&
c.isFile(),
c.name.toLowerCase() === `${name.toLowerCase()}.exe` && c.isFile(),
)[0].name,
);
@ -130,7 +135,9 @@ export function getOptionsFromExecutable(
'ProductVersion',
);
log.debug(
`Extracted app version from executable: ${newOptions.appVersion}`,
`Extracted app version from executable: ${
newOptions.appVersion as string
}`,
);
}
@ -142,7 +149,9 @@ export function getOptionsFromExecutable(
newOptions.buildVersion = undefined;
} else {
log.debug(
`Extracted build version from executable: ${newOptions.buildVersion}`,
`Extracted build version from executable: ${
newOptions.buildVersion as string
}`,
);
}
}
@ -154,7 +163,9 @@ export function getOptionsFromExecutable(
'LegalCopyright',
);
log.debug(
`Extracted app copyright from executable: ${newOptions.appCopyright}`,
`Extracted app copyright from executable: ${
newOptions.appCopyright as string
}`,
);
}
} else if (looksLikeLinux) {
@ -164,7 +175,13 @@ export function getOptionsFromExecutable(
}
executablePath = path.join(
appRoot,
children.filter((c) => c.name == newOptions.name && c.isFile())[0].name,
children.filter((c) => c.name == name && c.isFile())[0].name,
);
}
if (!executablePath || !newOptions.platform) {
throw Error(
`Could not find executablePath or platform of app in ${appRoot}`,
);
}
@ -175,8 +192,14 @@ export function getOptionsFromExecutable(
executablePath,
newOptions.platform,
);
if (!executableInfo) {
throw new Error(
`Could not get executable info for executable path: ${executablePath}`,
);
}
newOptions.arch = executableInfo.arch;
log.debug(`Extracted arch from executable: ${newOptions.arch}`);
log.debug(`Extracted arch from executable: ${newOptions.arch as string}`);
}
if (newOptions.platform === undefined || newOptions.arch == undefined) {
throw Error(`Could not determine platform / arch of app in ${appRoot}`);

View File

@ -8,7 +8,7 @@ import { spawnSync } from 'child_process';
export function getVersionString(
executablePath: string,
versionString: string,
): string {
): string | undefined {
let rcedit = path.resolve(
__dirname,
'..',

View File

@ -3,10 +3,11 @@ import * as path from 'path';
import * as log from 'loglevel';
import { NativefierOptions } from '../../options/model';
import { NativefierOptions, RawOptions } from '../../options/model';
import { dirExists, fileExists } from '../fsHelpers';
import { extractBoolean, extractString } from './plistInfoXMLHelpers';
import { getOptionsFromExecutable } from './executableHelpers';
import { parseJson } from '../../utils/parseUtils';
export type UpgradeAppInfo = {
appResourcesDir: string;
@ -79,7 +80,9 @@ function getInfoPListOptions(
'NSHumanReadableCopyright',
);
log.debug(
`Extracted app copyright from Info.plist: ${newOptions.appCopyright}`,
`Extracted app copyright from Info.plist: ${
newOptions.appCopyright as string
}`,
);
}
@ -96,7 +99,9 @@ function getInfoPListOptions(
? undefined
: newOptions.darwinDarkModeSupport === false),
log.debug(
`Extracted app version from Info.plist: ${newOptions.appVersion}`,
`Extracted app version from Info.plist: ${
newOptions.appVersion as string
}`,
);
}
@ -152,11 +157,19 @@ export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null {
return null;
}
log.debug(`Loading ${path.join(appResourcesDir, 'nativefier.json')}`);
const options: NativefierOptions = JSON.parse(
fs.readFileSync(path.join(appResourcesDir, 'nativefier.json'), 'utf8'),
const nativefierJSONPath = path.join(appResourcesDir, 'nativefier.json');
log.debug(`Loading ${nativefierJSONPath}`);
const options = parseJson<NativefierOptions>(
fs.readFileSync(nativefierJSONPath, 'utf8'),
);
if (!options) {
throw new Error(
`Could not read Nativefier options from ${nativefierJSONPath}`,
);
}
options.electronVersion = undefined;
return {
@ -173,9 +186,9 @@ export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null {
}
export function useOldAppOptions(
rawOptions: NativefierOptions,
rawOptions: RawOptions,
oldApp: UpgradeAppInfo,
): NativefierOptions {
): RawOptions {
if (rawOptions.targetUrl !== undefined && dirExists(rawOptions.targetUrl)) {
// You got your ouput dir in my targetUrl!
rawOptions.out = rawOptions.targetUrl;

View File

@ -31,7 +31,7 @@ export async function getChromeVersionForElectronVersion(
try {
log.debug('Grabbing electron<->chrome versions file from', url);
const response = await axios.get(url, { timeout: 5000 });
const response = await axios.get<ElectronRelease[]>(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
@ -50,7 +50,7 @@ export async function getChromeVersionForElectronVersion(
`Associated electron v${electronVersion} to chrome v${chromeVersion}`,
);
return chromeVersion;
} catch (err) {
} catch (err: unknown) {
log.error('getChromeVersionForElectronVersion ERROR', err);
log.debug('Falling back to default Chrome version', DEFAULT_CHROME_VERSION);
return DEFAULT_CHROME_VERSION;

View File

@ -28,7 +28,7 @@ export async function getLatestFirefoxVersion(
): Promise<string> {
try {
log.debug('Grabbing Firefox version data from', url);
const response = await axios.get(url, { timeout: 5000 });
const response = await axios.get<FirefoxVersions>(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
@ -38,7 +38,7 @@ export async function getLatestFirefoxVersion(
`Got latest Firefox version ${firefoxVersions.LATEST_FIREFOX_VERSION}`,
);
return firefoxVersions.LATEST_FIREFOX_VERSION;
} catch (err) {
} catch (err: unknown) {
log.error('getLatestFirefoxVersion ERROR', err);
log.debug(
'Falling back to default Firefox version',

View File

@ -16,7 +16,7 @@ export async function getLatestSafariVersion(
): Promise<SafariVersion> {
try {
log.debug('Grabbing apple version data from', url);
const response = await axios.get(url, { timeout: 5000 });
const response = await axios.get<string>(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
@ -39,8 +39,8 @@ export async function getLatestSafariVersion(
const versionRows = majorVersionTable.split('<tbody')[1].split('<tr');
let version = null;
let webkitVersion: string = null;
let version: string | undefined = undefined;
let webkitVersion: string | undefined = undefined;
for (const versionRow of versionRows.reverse()) {
const versionMatch = [
@ -61,12 +61,15 @@ export async function getLatestSafariVersion(
}
}
return {
majorVersion,
version,
webkitVersion,
};
} catch (err) {
if (version && webkitVersion) {
return {
majorVersion,
version,
webkitVersion,
};
}
return DEFAULT_SAFARI_VERSION;
} catch (err: unknown) {
log.error('getLatestSafariVersion ERROR', err);
log.debug('Falling back to default Safari version', DEFAULT_SAFARI_VERSION);
return DEFAULT_SAFARI_VERSION;

View File

@ -7,6 +7,7 @@ import * as pageIcon from 'page-icon';
import {
downloadFile,
DownloadResult,
getAllowedIconFormats,
getTempDir,
} from '../helpers/helpers';
@ -17,10 +18,17 @@ const writeFileAsync = promisify(writeFile);
const GITCLOUD_SPACE_DELIMITER = '-';
const GITCLOUD_URL = 'https://nativefier.github.io/nativefier-icons/';
function getMaxMatchScore(iconWithScores: any[]): number {
type GitCloudIcon = {
ext?: string;
name?: string;
score?: number;
url?: string;
};
function getMaxMatchScore(iconWithScores: GitCloudIcon[]): number {
const score = iconWithScores.reduce((maxScore, currentIcon) => {
const currentScore = currentIcon.score;
if (currentScore > maxScore) {
if (currentScore && currentScore > maxScore) {
return currentScore;
}
return maxScore;
@ -29,36 +37,42 @@ function getMaxMatchScore(iconWithScores: any[]): number {
return score;
}
function getMatchingIcons(iconsWithScores: any[], maxScore: number): any[] {
return iconsWithScores
.filter((item) => item.score === maxScore)
.map((item) => ({ ...item, ext: path.extname(item.url) }));
function getMatchingIcons(
iconsWithScores: GitCloudIcon[],
maxScore: number,
): GitCloudIcon[] {
return iconsWithScores.filter((item) => item.score === maxScore);
}
function mapIconWithMatchScore(
cloudIcons: { name: string; url: string }[],
targetUrl: string,
): any {
): GitCloudIcon[] {
const normalisedTargetUrl = targetUrl.toLowerCase();
return cloudIcons.map((item) => {
const itemWords = item.name.split(GITCLOUD_SPACE_DELIMITER);
const score = itemWords.reduce((currentScore: number, word: string) => {
if (normalisedTargetUrl.includes(word)) {
return currentScore + 1;
}
return currentScore;
}, 0);
const score: number = itemWords.reduce(
(currentScore: number, word: string) => {
if (normalisedTargetUrl.includes(word)) {
return currentScore + 1;
}
return currentScore;
},
0,
);
return { ...item, score };
return { ...item, ext: path.extname(item.url), score };
});
}
async function inferIconFromStore(
targetUrl: string,
platform: string,
): Promise<any> {
): Promise<DownloadResult | undefined> {
log.debug(`Inferring icon from store for ${targetUrl} on ${platform}`);
const allowedFormats = new Set(getAllowedIconFormats(platform));
const allowedFormats = new Set<string | undefined>(
getAllowedIconFormats(platform),
);
const cloudIcons = await gitCloud(GITCLOUD_URL);
log.debug(`Got ${cloudIcons.length} icons from gitcloud`);
@ -67,19 +81,19 @@ async function inferIconFromStore(
if (maxScore === 0) {
log.debug('No relevant icon in store.');
return null;
return undefined;
}
const iconsMatchingScore = getMatchingIcons(iconWithScores, maxScore);
const iconsMatchingExt = iconsMatchingScore.filter((icon) =>
allowedFormats.has(icon.ext),
allowedFormats.has(icon.ext ?? path.extname(icon.url as string)),
);
const matchingIcon = iconsMatchingExt[0];
const iconUrl = matchingIcon && matchingIcon.url;
if (!iconUrl) {
log.debug('Could not infer icon from store');
return null;
return undefined;
}
return downloadFile(iconUrl);
}
@ -87,21 +101,19 @@ async function inferIconFromStore(
export async function inferIcon(
targetUrl: string,
platform: string,
): Promise<string> {
): Promise<string | undefined> {
log.debug(`Inferring icon for ${targetUrl} on ${platform}`);
const tmpDirPath = getTempDir('iconinfer');
let icon: { ext: string; data: Buffer } = await inferIconFromStore(
targetUrl,
platform,
);
let icon: { ext: string; data: Buffer } | undefined =
await inferIconFromStore(targetUrl, platform);
if (!icon) {
const ext = platform === 'win32' ? '.ico' : '.png';
log.debug(`Trying to extract a ${ext} icon from the page.`);
icon = await pageIcon(targetUrl, { ext });
}
if (!icon) {
return null;
return undefined;
}
log.debug(`Got an icon from the page.`);

View File

@ -19,13 +19,7 @@ export const supportedPlatforms = [
export function inferPlatform(): string {
const platform = os.platform();
if (
platform === 'darwin' ||
// @ts-ignore
platform === 'mas' ||
platform === 'win32' ||
platform === 'linux'
) {
if (['darwin', 'linux', 'win32'].includes(platform)) {
log.debug('Inferred platform', platform);
return platform;
}

View File

@ -5,7 +5,7 @@ const USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36';
export async function inferTitle(url: string): Promise<string> {
const { data } = await axios.get(url, {
const { data } = await axios.get<string>(url, {
headers: {
// Fake user agent for pages like http://messenger.com
'User-Agent': USER_AGENT,
@ -13,7 +13,7 @@ export async function inferTitle(url: string): Promise<string> {
});
log.debug(`Fetched ${(data.length / 1024).toFixed(1)} kb page at`, url);
const inferredTitle =
/<\s*title.*?>(?<title>.+?)<\s*\/title\s*?>/i.exec(data).groups?.title ||
/<\s*title.*?>(?<title>.+?)<\s*\/title\s*?>/i.exec(data)?.groups?.title ??
'Webapp';
log.debug('Inferred title:', inferredTitle);
return inferredTitle;

View File

@ -10,9 +10,14 @@ import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion';
import { inferArch } from './infer/inferOs';
import { buildNativefierApp } from './main';
import { userAgent } from './options/fields/userAgent';
import { NativefierOptions } from './options/model';
import { parseJson } from './utils/parseUtils';
async function checkApp(appRoot: string, inputOptions: any): Promise<void> {
const arch = (inputOptions.arch as string) || inferArch();
async function checkApp(
appRoot: string,
inputOptions: NativefierOptions,
): Promise<void> {
const arch = inputOptions.arch ? (inputOptions.arch as string) : inferArch();
if (inputOptions.out !== undefined) {
expect(
path.join(
@ -43,28 +48,31 @@ async function checkApp(appRoot: string, inputOptions: any): Promise<void> {
const appPath = path.join(appRoot, relativeAppFolder);
const configPath = path.join(appPath, 'nativefier.json');
const nativefierConfig = JSON.parse(fs.readFileSync(configPath).toString());
expect(inputOptions.targetUrl).toBe(nativefierConfig.targetUrl);
const nativefierConfig: NativefierOptions | undefined =
parseJson<NativefierOptions>(fs.readFileSync(configPath).toString());
expect(nativefierConfig).not.toBeUndefined();
expect(inputOptions.targetUrl).toBe(nativefierConfig?.targetUrl);
// Test name inferring
expect(nativefierConfig.name).toBe('Google');
expect(nativefierConfig?.name).toBe('Google');
// Test icon writing
const iconFile =
inputOptions.platform === 'darwin' ? '../electron.icns' : 'icon.png';
const iconPath = path.join(appPath, iconFile);
expect(fs.existsSync(iconPath)).toBe(true);
expect(fs.existsSync(iconPath)).toEqual(true);
expect(fs.statSync(iconPath).size).toBeGreaterThan(1000);
// Test arch
if (inputOptions.arch !== undefined) {
expect(inputOptions.arch).toBe(nativefierConfig.arch);
expect(inputOptions.arch).toEqual(nativefierConfig?.arch);
} else {
expect(os.arch()).toBe(nativefierConfig.arch);
expect(os.arch()).toEqual(nativefierConfig?.arch);
}
// Test electron version
expect(nativefierConfig.electronVersionUsed).toBe(
expect(nativefierConfig?.electronVersionUsed).toBe(
inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION,
);
@ -80,10 +88,11 @@ async function checkApp(appRoot: string, inputOptions: any): Promise<void> {
});
inputOptions.userAgent = translatedUserAgent || inputOptions.userAgent;
}
expect(nativefierConfig.userAgent).toBe(inputOptions.userAgent);
expect(nativefierConfig?.userAgent).toEqual(inputOptions.userAgent);
// Test lang
expect(nativefierConfig.lang).toBe(inputOptions.lang);
expect(nativefierConfig?.lang).toEqual(inputOptions.lang);
}
describe('Nativefier', () => {
@ -101,7 +110,8 @@ describe('Nativefier', () => {
lang: 'en-US',
};
const appPath = await buildNativefierApp(options);
await checkApp(appPath, options);
expect(appPath).not.toBeUndefined();
await checkApp(appPath as string, options);
},
);
});
@ -132,7 +142,8 @@ describe('Nativefier upgrade', () => {
...baseAppOptions,
};
const appPath = await buildNativefierApp(options);
await checkApp(appPath, options);
expect(appPath).not.toBeUndefined();
await checkApp(appPath as string, options);
const upgradeOptions = {
upgrade: appPath,
@ -142,7 +153,8 @@ describe('Nativefier upgrade', () => {
const upgradeAppPath = await buildNativefierApp(upgradeOptions);
options.electronVersion = DEFAULT_ELECTRON_VERSION;
options.userAgent = baseAppOptions.userAgent;
await checkApp(upgradeAppPath, options);
expect(upgradeAppPath).not.toBeUndefined();
await checkApp(upgradeAppPath as string, options);
},
);
});

View File

@ -1,6 +1,7 @@
import 'source-map-support/register';
import { buildNativefierApp } from './build/buildNativefierApp';
import { RawOptions } from './options/model';
export { buildNativefierApp };
@ -9,12 +10,12 @@ export { buildNativefierApp };
* Use the better, modern async `buildNativefierApp` instead if you can!
*/
function buildNativefierAppOldCallbackStyle(
options: any, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
callback: (err: any, result?: any) => void,
options: RawOptions, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
callback: (err?: Error, result?: string) => void,
): void {
buildNativefierApp(options)
.then((result) => callback(null, result))
.catch((err) => callback(err));
.then((result) => callback(undefined, result))
.catch((err: Error) => callback(err));
}
export default buildNativefierAppOldCallbackStyle;

View File

@ -6,7 +6,7 @@ import { AppOptions } from './model';
/**
* Takes the options object and infers new values needing async work
*/
export async function asyncConfig(options: AppOptions): Promise<any> {
export async function asyncConfig(options: AppOptions): Promise<AppOptions> {
log.debug('\nPerforming async options post-processing.');
await processOptions(options);
return await processOptions(options);
}

View File

@ -1,53 +1,103 @@
import { AppOptions } from '../model';
import { processOptions } from './fields';
describe('fields', () => {
let options: AppOptions;
test('fully-defined async options are returned as-is', async () => {
const options = {
packager: {
icon: '/my/icon.png',
name: 'my beautiful app ',
targetUrl: 'https://myurl.com',
dir: '/tmp/myapp',
},
nativefier: { userAgent: 'random user agent' },
};
// @ts-ignore
await processOptions(options);
beforeEach(() => {
options = {
nativefier: {
accessibilityPrompt: false,
alwaysOnTop: false,
backgroundColor: undefined,
basicAuthPassword: undefined,
basicAuthUsername: undefined,
blockExternalUrls: false,
bookmarksMenu: undefined,
bounce: false,
browserwindowOptions: undefined,
clearCache: false,
counter: false,
crashReporter: undefined,
disableContextMenu: false,
disableDevTools: false,
disableGpu: false,
disableOldBuildWarning: false,
diskCacheSize: undefined,
enableEs3Apis: false,
fastQuit: true,
fileDownloadOptions: undefined,
flashPluginDir: undefined,
fullScreen: false,
globalShortcuts: undefined,
height: undefined,
hideWindowFrame: false,
ignoreCertificate: false,
ignoreGpuBlacklist: false,
inject: [],
insecure: false,
internalUrls: undefined,
maximize: false,
maxHeight: undefined,
minWidth: undefined,
minHeight: undefined,
maxWidth: undefined,
nativefierVersion: '1.0.0',
processEnvs: undefined,
proxyRules: undefined,
showMenuBar: false,
singleInstance: false,
titleBarStyle: undefined,
tray: false,
userAgent: undefined,
userAgentHonest: false,
verbose: false,
versionString: '1.0.0',
width: undefined,
widevine: false,
x: undefined,
y: undefined,
zoom: 1,
},
packager: {
dir: '',
platform: process.platform,
portable: false,
targetUrl: '',
upgrade: false,
},
};
});
expect(options.packager.icon).toEqual('/my/icon.png');
expect(options.packager.name).toEqual('my beautiful app');
expect(options.nativefier.userAgent).toEqual('random user agent');
});
test('user agent is ignored if not provided', async () => {
const options = {
packager: {
icon: '/my/icon.png',
name: 'my beautiful app ',
targetUrl: 'https://myurl.com',
dir: '/tmp/myapp',
platform: 'linux',
},
nativefier: { userAgent: undefined },
};
// @ts-ignore
await processOptions(options);
expect(options.nativefier.userAgent).toBeUndefined();
});
test('user agent short code is populated', async () => {
const options = {
packager: {
icon: '/my/icon.png',
name: 'my beautiful app ',
targetUrl: 'https://myurl.com',
dir: '/tmp/myapp',
platform: 'linux',
},
nativefier: { userAgent: 'edge' },
};
// @ts-ignore
await processOptions(options);
expect(options.nativefier.userAgent).not.toBe('edge');
test('fully-defined async options are returned as-is', async () => {
options.packager.icon = '/my/icon.png';
options.packager.name = 'my beautiful app ';
options.packager.platform = 'darwin';
options.nativefier.userAgent = 'random user agent';
await processOptions(options);
expect(options.packager.icon).toEqual('/my/icon.png');
expect(options.packager.name).toEqual('my beautiful app');
expect(options.nativefier.userAgent).toEqual('random user agent');
});
test('name has spaces stripped in linux', async () => {
options.packager.name = 'my beautiful app ';
options.packager.platform = 'linux';
await processOptions(options);
expect(options.packager.name).toEqual('mybeautifulapp');
});
test('user agent is ignored if not provided', async () => {
await processOptions(options);
expect(options.nativefier.userAgent).toBeUndefined();
});
test('user agent short code is populated', async () => {
options.nativefier.userAgent = 'edge';
await processOptions(options);
expect(options.nativefier.userAgent).not.toBe('edge');
});
});

View File

@ -3,13 +3,19 @@ import { userAgent } from './userAgent';
import { AppOptions } from '../model';
import { name } from './name';
const OPTION_POSTPROCESSORS = [
type OptionPostprocessor = {
namespace: 'nativefier' | 'packager';
option: 'icon' | 'name' | 'userAgent';
processor: (options: AppOptions) => Promise<string | undefined>;
};
const OPTION_POSTPROCESSORS: OptionPostprocessor[] = [
{ namespace: 'nativefier', option: 'userAgent', processor: userAgent },
{ namespace: 'packager', option: 'icon', processor: icon },
{ namespace: 'packager', option: 'name', processor: name },
];
export async function processOptions(options: AppOptions): Promise<void> {
export async function processOptions(options: AppOptions): Promise<AppOptions> {
const processedOptions = await Promise.all(
OPTION_POSTPROCESSORS.map(async ({ namespace, option, processor }) => {
const result = await processor(options);
@ -22,8 +28,15 @@ export async function processOptions(options: AppOptions): Promise<void> {
);
for (const { namespace, option, result } of processedOptions) {
if (result !== null) {
if (
result &&
namespace in options &&
options[namespace] &&
option in options[namespace]
) {
// @ts-expect-error We're fiddling with objects at the string key level, which TS doesn't support well.
options[namespace][option] = result;
}
}
return options;
}

View File

@ -24,7 +24,7 @@ const ICON_PARAMS_NEEDS_INFER = {
describe('when the icon parameter is passed', () => {
test('it should return the icon parameter', async () => {
expect(inferIcon).toHaveBeenCalledTimes(0);
await expect(icon(ICON_PARAMS_PROVIDED)).resolves.toBe(null);
await expect(icon(ICON_PARAMS_PROVIDED)).resolves.toBeUndefined();
});
});
@ -49,7 +49,7 @@ describe('when the icon parameter is not passed', () => {
);
const result = await icon(ICON_PARAMS_NEEDS_INFER);
expect(result).toBe(null);
expect(result).toBeUndefined();
expect(inferIcon).toHaveBeenCalledWith(
ICON_PARAMS_NEEDS_INFER.packager.targetUrl,
ICON_PARAMS_NEEDS_INFER.packager.platform,

View File

@ -10,10 +10,15 @@ type IconParams = {
};
};
export async function icon(options: IconParams): Promise<string> {
export async function icon(options: IconParams): Promise<string | undefined> {
if (options.packager.icon) {
log.debug('Got icon from options. Using it, no inferring needed');
return null;
return undefined;
}
if (!options.packager.platform) {
log.error('No platform specified. Icon can not be inferrerd.');
return undefined;
}
try {
@ -21,11 +26,8 @@ export async function icon(options: IconParams): Promise<string> {
options.packager.targetUrl,
options.packager.platform,
);
} catch (error) {
log.warn(
'Cannot automatically retrieve the app icon:',
error.message || '',
);
return null;
} catch (err: unknown) {
log.warn('Cannot automatically retrieve the app icon:', err);
return undefined;
}
}

View File

@ -17,24 +17,20 @@ async function tryToInferName(targetUrl: string): Promise<string> {
log.debug('Inferring name for', targetUrl);
const pageTitle = await inferTitle(targetUrl);
return pageTitle || DEFAULT_APP_NAME;
} catch (err) {
} catch (err: unknown) {
log.warn(
`Unable to automatically determine app name, falling back to '${DEFAULT_APP_NAME}'. Reason: ${(
err as Error
).toString()}`,
`Unable to automatically determine app name, falling back to '${DEFAULT_APP_NAME}'.`,
err,
);
return DEFAULT_APP_NAME;
}
}
export async function name(options: NameParams): Promise<string> {
if (options.packager.name) {
log.debug(
`Got name ${options.packager.name} from options. No inferring needed`,
);
return sanitizeFilename(options.packager.platform, options.packager.name);
let name: string | undefined = options.packager.name;
if (!name) {
name = await tryToInferName(options.packager.targetUrl);
}
const inferredName = await tryToInferName(options.packager.targetUrl);
return sanitizeFilename(options.packager.platform, inferredName);
return sanitizeFilename(options.packager.platform, name);
}

View File

@ -12,7 +12,7 @@ test('when a userAgent parameter is passed', async () => {
packager: {},
nativefier: { userAgent: 'valid user agent' },
};
await expect(userAgent(params)).resolves.toBeNull();
await expect(userAgent(params)).resolves.toBeUndefined();
});
test('no userAgent parameter is passed', async () => {
@ -20,7 +20,7 @@ test('no userAgent parameter is passed', async () => {
packager: { platform: 'mac' },
nativefier: {},
};
await expect(userAgent(params)).resolves.toBeNull();
await expect(userAgent(params)).resolves.toBeUndefined();
});
test('edge userAgent parameter is passed', async () => {

View File

@ -1,11 +1,12 @@
import * as log from 'loglevel';
import { DEFAULT_ELECTRON_VERSION } from '../../constants';
import { getChromeVersionForElectronVersion } from '../../infer/browsers/inferChromeVersion';
import { getLatestFirefoxVersion } from '../../infer/browsers/inferFirefoxVersion';
import { getLatestSafariVersion } from '../../infer/browsers/inferSafariVersion';
import { normalizePlatform } from '../optionsMain';
type UserAgentOpts = {
export type UserAgentOpts = {
packager: {
electronVersion?: string;
platform?: string;
@ -15,22 +16,27 @@ type UserAgentOpts = {
};
};
const USER_AGENT_PLATFORM_MAPS = {
const USER_AGENT_PLATFORM_MAPS: Record<string, string> = {
darwin: 'Macintosh; Intel Mac OS X 10_15_7',
linux: 'X11; Linux x86_64',
win32: 'Windows NT 10.0; Win64; x64',
};
const USER_AGENT_SHORT_CODE_MAPS = {
const USER_AGENT_SHORT_CODE_MAPS: Record<
string,
(platform: string, electronVersion: string) => Promise<string>
> = {
edge: edgeUserAgent,
firefox: firefoxUserAgent,
safari: safariUserAgent,
};
export async function userAgent(options: UserAgentOpts): Promise<string> {
export async function userAgent(
options: UserAgentOpts,
): Promise<string | undefined> {
if (!options.nativefier.userAgent) {
// No user agent got passed. Let's handle it with the app.userAgentFallback
return null;
return undefined;
}
if (
@ -43,19 +49,22 @@ export async function userAgent(options: UserAgentOpts): Promise<string> {
`${options.nativefier.userAgent.toLowerCase()} not found in`,
Object.keys(USER_AGENT_SHORT_CODE_MAPS),
);
return null;
return undefined;
}
options.packager.platform = normalizePlatform(options.packager.platform);
const userAgentPlatform =
const userAgentPlatform: string =
USER_AGENT_PLATFORM_MAPS[
options.packager.platform === 'mas' ? 'darwin' : options.packager.platform
];
const mapFunction = USER_AGENT_SHORT_CODE_MAPS[options.nativefier.userAgent];
return await mapFunction(userAgentPlatform, options.packager.electronVersion);
return await mapFunction(
userAgentPlatform,
options.packager.electronVersion ?? DEFAULT_ELECTRON_VERSION,
);
}
async function edgeUserAgent(

View File

@ -1,8 +1,9 @@
import { CreateOptions } from 'asar';
import * as electronPackager from 'electron-packager';
export interface ElectronPackagerOptions extends electronPackager.Options {
portable: boolean;
platform: string;
platform?: string;
targetUrl: string;
upgrade: boolean;
upgradeFrom?: string;
@ -13,60 +14,152 @@ export interface AppOptions {
nativefier: {
accessibilityPrompt: boolean;
alwaysOnTop: boolean;
backgroundColor: string;
basicAuthPassword: string;
basicAuthUsername: string;
backgroundColor?: string;
basicAuthPassword?: string;
basicAuthUsername?: string;
blockExternalUrls: boolean;
bookmarksMenu: string;
bookmarksMenu?: string;
bounce: boolean;
browserwindowOptions: any;
browserwindowOptions?: BrowserWindowOptions;
clearCache: boolean;
counter: boolean;
crashReporter: string;
crashReporter?: string;
disableContextMenu: boolean;
disableDevTools: boolean;
disableGpu: boolean;
disableOldBuildWarning: boolean;
diskCacheSize: number;
diskCacheSize?: number;
electronVersionUsed?: string;
enableEs3Apis: boolean;
fastQuit: boolean;
fileDownloadOptions: any;
flashPluginDir: string;
fileDownloadOptions: unknown;
flashPluginDir?: string;
fullScreen: boolean;
globalShortcuts: any;
globalShortcuts?: GlobalShortcut[];
hideWindowFrame: boolean;
ignoreCertificate: boolean;
ignoreGpuBlacklist: boolean;
inject: string[];
inject?: string[];
insecure: boolean;
internalUrls: string;
internalUrls?: string;
lang?: string;
maximize: boolean;
nativefierVersion: string;
processEnvs: string;
proxyRules: string;
nativefierVersion?: string;
processEnvs?: string;
proxyRules?: string;
showMenuBar: boolean;
singleInstance: boolean;
titleBarStyle: string;
titleBarStyle?: string;
tray: string | boolean;
userAgent: string;
userAgent?: string;
userAgentHonest: boolean;
verbose: boolean;
versionString: string;
width: number;
versionString?: string;
width?: number;
widevine: boolean;
height: number;
minWidth: number;
minHeight: number;
maxWidth: number;
maxHeight: number;
x: number;
y: number;
height?: number;
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
x?: number;
y?: number;
zoom: number;
};
}
export type BrowserWindowOptions = Record<string, unknown>;
export type GlobalShortcut = {
key: string;
};
export type NativefierOptions = Partial<
AppOptions['packager'] & AppOptions['nativefier']
>;
export type OutputOptions = NativefierOptions & {
buildDate: number;
isUpgrade: boolean;
oldBuildWarningText: string;
};
export type PackageJSON = {
name: string;
};
export type RawOptions = {
accessibilityPrompt?: boolean;
alwaysOnTop?: boolean;
appCopyright?: string;
appVersion?: string;
arch?: string | string[];
asar?: boolean | CreateOptions;
backgroundColor?: string;
basicAuthPassword?: string;
basicAuthUsername?: string;
blockExternalUrls?: boolean;
bookmarksMenu?: string;
bounce?: boolean;
browserwindowOptions?: BrowserWindowOptions;
buildVersion?: string;
clearCache?: boolean;
conceal?: boolean;
counter?: boolean;
crashReporter?: string;
darwinDarkModeSupport?: boolean;
disableContextMenu?: boolean;
disableDevTools?: boolean;
disableGpu?: boolean;
disableOldBuildWarning?: boolean;
disableOldBuildWarningYesiknowitisinsecure?: boolean;
diskCacheSize?: number;
electronVersion?: string;
electronVersionUsed?: string;
enableEs3Apis?: boolean;
fastQuit?: boolean;
fileDownloadOptions?: unknown;
flashPath?: string;
flashPluginDir?: string;
fullScreen?: boolean;
globalShortcuts?: string | GlobalShortcut[];
height?: number;
hideWindowFrame?: boolean;
icon?: string;
ignoreCertificate?: boolean;
ignoreGpuBlacklist?: boolean;
inject?: string[];
insecure?: boolean;
internalUrls?: string;
lang?: string;
maxHeight?: number;
maximize?: boolean;
maxWidth?: number;
minHeight?: number;
minWidth?: number;
name?: string;
nativefierVersion?: string;
out?: string;
overwrite?: boolean;
platform?: string;
portable?: boolean;
processEnvs?: string;
proxyRules?: string;
showMenuBar?: boolean;
singleInstance?: boolean;
targetUrl?: string;
titleBarStyle?: string;
tray?: string | boolean;
upgrade?: string | boolean;
upgradeFrom?: string;
userAgent?: string;
userAgentHonest?: boolean;
verbose?: boolean;
versionString?: string;
widevine?: boolean;
width?: number;
win32metadata?: electronPackager.Win32MetadataOptions;
x?: number;
y?: number;
zoom?: number;
};

View File

@ -22,8 +22,9 @@ export function normalizeUrl(urlToNormalize: string): string {
let parsedUrl: url.URL;
try {
parsedUrl = new url.URL(urlWithProtocol);
} catch (err) {
throw `Your url "${urlWithProtocol}" is invalid`;
} catch (err: unknown) {
log.error('normalizeUrl ERROR', err);
throw new Error(`Your url "${urlWithProtocol}" is invalid`);
}
const normalizedUrl = parsedUrl.toString();
log.debug(`Normalized URL ${urlToNormalize} to:`, normalizedUrl);

View File

@ -1,9 +1,71 @@
import { getOptions, normalizePlatform } from './optionsMain';
import * as asyncConfig from './asyncConfig';
import { inferPlatform } from '../infer/inferOs';
import { AppOptions } from './model';
const mockedAsyncConfig = { some: 'options' };
let asyncConfigMock: jest.SpyInstance;
const mockedAsyncConfig: AppOptions = {
nativefier: {
accessibilityPrompt: false,
alwaysOnTop: false,
backgroundColor: undefined,
basicAuthPassword: undefined,
basicAuthUsername: undefined,
blockExternalUrls: false,
bookmarksMenu: undefined,
bounce: false,
browserwindowOptions: undefined,
clearCache: false,
counter: false,
crashReporter: undefined,
disableContextMenu: false,
disableDevTools: false,
disableGpu: false,
disableOldBuildWarning: false,
diskCacheSize: undefined,
enableEs3Apis: false,
fastQuit: true,
fileDownloadOptions: undefined,
flashPluginDir: undefined,
fullScreen: false,
globalShortcuts: undefined,
height: undefined,
hideWindowFrame: false,
ignoreCertificate: false,
ignoreGpuBlacklist: false,
inject: [],
insecure: false,
internalUrls: undefined,
maximize: false,
maxHeight: undefined,
minWidth: undefined,
minHeight: undefined,
maxWidth: undefined,
nativefierVersion: '1.0.0',
processEnvs: undefined,
proxyRules: undefined,
showMenuBar: false,
singleInstance: false,
titleBarStyle: undefined,
tray: false,
userAgent: undefined,
userAgentHonest: false,
verbose: false,
versionString: '1.0.0',
width: undefined,
widevine: false,
x: undefined,
y: undefined,
zoom: 1,
},
packager: {
dir: '',
platform: process.platform,
portable: false,
targetUrl: '',
upgrade: false,
},
};
beforeAll(() => {
asyncConfigMock = jest
@ -18,8 +80,8 @@ test('it should call the async config', async () => {
const result = await getOptions(params);
expect(asyncConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
packager: expect.anything(),
nativefier: expect.anything(),
packager: expect.anything() as AppOptions['packager'],
nativefier: expect.anything() as AppOptions['nativefier'],
}),
);
expect(result.packager.targetUrl).toEqual(params.targetUrl);
@ -34,7 +96,7 @@ test('it should set the accessibility prompt option to true by default', async (
expect.objectContaining({
nativefier: expect.objectContaining({
accessibilityPrompt: true,
}),
}) as AppOptions['nativefier'],
}),
);
expect(result.nativefier.accessibilityPrompt).toEqual(true);

View File

@ -1,11 +1,17 @@
import * as fs from 'fs';
import axios from 'axios';
import * as debug from 'debug';
import * as log from 'loglevel';
// package.json is `require`d to let tsc strip the `src` folder by determining
// baseUrl=src. A static import would prevent that and cause an ugly extra `src` folder in `lib`
const packageJson = require('../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const packageJson: {
name: string;
version: string;
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require('../../package.json');
import {
DEFAULT_ELECTRON_VERSION,
PLACEHOLDER_APP_DIR,
@ -13,8 +19,9 @@ import {
} from '../constants';
import { inferPlatform, inferArch } from '../infer/inferOs';
import { asyncConfig } from './asyncConfig';
import { AppOptions } from './model';
import { AppOptions, GlobalShortcut, RawOptions } from './model';
import { normalizeUrl } from './normalizeUrl';
import { parseJson } from '../utils/parseUtils';
const SEMVER_VERSION_NUMBER_REGEX = /\d+\.\d+\.\d+[-_\w\d.]*/;
@ -22,31 +29,33 @@ const SEMVER_VERSION_NUMBER_REGEX = /\d+\.\d+\.\d+[-_\w\d.]*/;
* Process and validate raw user arguments
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function getOptions(rawOptions: any): Promise<AppOptions> {
export async function getOptions(rawOptions: RawOptions): Promise<AppOptions> {
const options: AppOptions = {
packager: {
appCopyright: rawOptions.appCopyright,
appVersion: rawOptions.appVersion,
arch: rawOptions.arch || inferArch(),
asar: rawOptions.asar || rawOptions.conceal || false,
arch: rawOptions.arch ?? inferArch(),
asar: rawOptions.asar ?? rawOptions.conceal ?? false,
buildVersion: rawOptions.buildVersion,
darwinDarkModeSupport: rawOptions.darwinDarkModeSupport || false,
darwinDarkModeSupport: rawOptions.darwinDarkModeSupport ?? false,
dir: PLACEHOLDER_APP_DIR,
electronVersion: rawOptions.electronVersion || DEFAULT_ELECTRON_VERSION,
electronVersion: rawOptions.electronVersion ?? DEFAULT_ELECTRON_VERSION,
icon: rawOptions.icon,
name: typeof rawOptions.name === 'string' ? rawOptions.name : '',
out: rawOptions.out || process.cwd(),
out: rawOptions.out ?? process.cwd(),
overwrite: rawOptions.overwrite,
platform: rawOptions.platform,
portable: rawOptions.portable || false,
portable: rawOptions.portable ?? false,
targetUrl:
rawOptions.targetUrl === undefined
? '' // We'll plug this in later via upgrade
: normalizeUrl(rawOptions.targetUrl),
tmpdir: false, // workaround for electron-packager#375
upgrade: rawOptions.upgrade !== undefined ? true : false,
upgradeFrom: rawOptions.upgrade,
win32metadata: rawOptions.win32metadata || {
upgradeFrom:
(rawOptions.upgradeFrom as string) ??
((rawOptions.upgrade as string) || undefined),
win32metadata: rawOptions.win32metadata ?? {
ProductName: rawOptions.name,
InternalName: rawOptions.name,
FileDescription: rawOptions.name,
@ -54,70 +63,70 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
},
nativefier: {
accessibilityPrompt: true,
alwaysOnTop: rawOptions.alwaysOnTop || false,
backgroundColor: rawOptions.backgroundColor || null,
basicAuthPassword: rawOptions.basicAuthPassword || null,
basicAuthUsername: rawOptions.basicAuthUsername || null,
blockExternalUrls: rawOptions.blockExternalUrls || false,
bookmarksMenu: rawOptions.bookmarksMenu || null,
bounce: rawOptions.bounce || false,
alwaysOnTop: rawOptions.alwaysOnTop ?? false,
backgroundColor: rawOptions.backgroundColor,
basicAuthPassword: rawOptions.basicAuthPassword,
basicAuthUsername: rawOptions.basicAuthUsername,
blockExternalUrls: rawOptions.blockExternalUrls ?? false,
bookmarksMenu: rawOptions.bookmarksMenu,
bounce: rawOptions.bounce ?? false,
browserwindowOptions: rawOptions.browserwindowOptions,
clearCache: rawOptions.clearCache || false,
counter: rawOptions.counter || false,
clearCache: rawOptions.clearCache ?? false,
counter: rawOptions.counter ?? false,
crashReporter: rawOptions.crashReporter,
disableContextMenu: rawOptions.disableContextMenu,
disableDevTools: rawOptions.disableDevTools,
disableGpu: rawOptions.disableGpu || false,
diskCacheSize: rawOptions.diskCacheSize || null,
disableContextMenu: rawOptions.disableContextMenu ?? false,
disableDevTools: rawOptions.disableDevTools ?? false,
disableGpu: rawOptions.disableGpu ?? false,
diskCacheSize: rawOptions.diskCacheSize,
disableOldBuildWarning:
rawOptions.disableOldBuildWarningYesiknowitisinsecure || false,
enableEs3Apis: rawOptions.enableEs3Apis || false,
fastQuit: rawOptions.fastQuit || false,
rawOptions.disableOldBuildWarningYesiknowitisinsecure ?? false,
enableEs3Apis: rawOptions.enableEs3Apis ?? false,
fastQuit: rawOptions.fastQuit ?? false,
fileDownloadOptions: rawOptions.fileDownloadOptions,
flashPluginDir: rawOptions.flashPath || rawOptions.flash || null,
fullScreen: rawOptions.fullScreen || false,
globalShortcuts: null,
hideWindowFrame: rawOptions.hideWindowFrame,
ignoreCertificate: rawOptions.ignoreCertificate || false,
ignoreGpuBlacklist: rawOptions.ignoreGpuBlacklist || false,
inject: rawOptions.inject || [],
insecure: rawOptions.insecure || false,
internalUrls: rawOptions.internalUrls || null,
lang: rawOptions.lang || undefined,
maximize: rawOptions.maximize || false,
flashPluginDir: rawOptions.flashPath,
fullScreen: rawOptions.fullScreen ?? false,
globalShortcuts: undefined,
hideWindowFrame: rawOptions.hideWindowFrame ?? false,
ignoreCertificate: rawOptions.ignoreCertificate ?? false,
ignoreGpuBlacklist: rawOptions.ignoreGpuBlacklist ?? false,
inject: rawOptions.inject ?? [],
insecure: rawOptions.insecure ?? false,
internalUrls: rawOptions.internalUrls,
lang: rawOptions.lang,
maximize: rawOptions.maximize ?? false,
nativefierVersion: packageJson.version,
processEnvs: rawOptions.processEnvs,
proxyRules: rawOptions.proxyRules || null,
showMenuBar: rawOptions.showMenuBar || false,
singleInstance: rawOptions.singleInstance || false,
titleBarStyle: rawOptions.titleBarStyle || null,
tray: rawOptions.tray || false,
proxyRules: rawOptions.proxyRules,
showMenuBar: rawOptions.showMenuBar ?? false,
singleInstance: rawOptions.singleInstance ?? false,
titleBarStyle: rawOptions.titleBarStyle,
tray: rawOptions.tray ?? false,
userAgent: rawOptions.userAgent,
userAgentHonest: rawOptions.userAgentHonest || false,
verbose: rawOptions.verbose,
userAgentHonest: rawOptions.userAgentHonest ?? false,
verbose: rawOptions.verbose ?? false,
versionString: rawOptions.versionString,
width: rawOptions.width || 1280,
height: rawOptions.height || 800,
width: rawOptions.width ?? 1280,
height: rawOptions.height ?? 800,
minWidth: rawOptions.minWidth,
minHeight: rawOptions.minHeight,
maxWidth: rawOptions.maxWidth,
maxHeight: rawOptions.maxHeight,
widevine: rawOptions.widevine || false,
widevine: rawOptions.widevine ?? false,
x: rawOptions.x,
y: rawOptions.y,
zoom: rawOptions.zoom || 1.0,
zoom: rawOptions.zoom ?? 1.0,
},
};
if (options.nativefier.verbose) {
log.setLevel('trace');
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('debug').enable('electron-packager');
} catch (err) {
log.debug(
debug.enable('electron-packager');
} catch (err: unknown) {
log.error(
'Failed to enable electron-packager debug output. This should not happen,',
'and suggests their internals changed. Please report an issue.',
err,
);
}
@ -145,13 +154,17 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
}
if (options.nativefier.widevine) {
const widevineElectronVersion = `${options.packager.electronVersion}-wvvmp`;
const widevineElectronVersion = `${
options.packager.electronVersion as string
}-wvvmp`;
try {
await axios.get(
`https://github.com/castlabs/electron-releases/releases/tag/v${widevineElectronVersion}`,
);
} catch (error) {
throw `\nERROR: castLabs Electron version "${widevineElectronVersion}" does not exist. \nVerify versions at https://github.com/castlabs/electron-releases/releases. \nAborting.`;
} catch {
throw new Error(
`\nERROR: castLabs Electron version "${widevineElectronVersion}" does not exist. \nVerify versions at https://github.com/castlabs/electron-releases/releases. \nAborting.`,
);
}
options.packager.electronVersion = widevineElectronVersion;
@ -173,7 +186,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
}
if (options.nativefier.userAgentHonest && options.nativefier.userAgent) {
options.nativefier.userAgent = null;
options.nativefier.userAgent = undefined;
log.warn(
`\nATTENTION: user-agent AND user-agent-honest/honest were provided. In this case, honesty wins. user-agent will be ignored`,
);
@ -181,11 +194,19 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
options.packager.platform = normalizePlatform(options.packager.platform);
if (options.nativefier.width > options.nativefier.maxWidth) {
if (
options.nativefier.maxWidth &&
options.nativefier.width &&
options.nativefier.width > options.nativefier.maxWidth
) {
options.nativefier.width = options.nativefier.maxWidth;
}
if (options.nativefier.height > options.nativefier.maxHeight) {
if (
options.nativefier.maxHeight &&
options.nativefier.height &&
options.nativefier.height > options.nativefier.maxHeight
) {
options.nativefier.height = options.nativefier.maxHeight;
}
@ -202,8 +223,8 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
if (rawOptions.globalShortcuts) {
log.debug('Using global shortcuts file at', rawOptions.globalShortcuts);
const globalShortcuts = JSON.parse(
fs.readFileSync(rawOptions.globalShortcuts).toString(),
const globalShortcuts = parseJson<GlobalShortcut[]>(
fs.readFileSync(rawOptions.globalShortcuts as string).toString(),
);
options.nativefier.globalShortcuts = globalShortcuts;
}
@ -213,7 +234,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
return options;
}
export function normalizePlatform(platform: string): string {
export function normalizePlatform(platform: string | undefined): string {
if (!platform) {
return inferPlatform();
}

View File

@ -14,8 +14,12 @@ test.each([
[undefined, true, true],
[undefined, false, false],
])(
'parseBoolean("%s") === %s',
(testString: string, expectedResult: boolean, _default: boolean) => {
expect(parseBoolean(testString, _default)).toBe(expectedResult);
'parseBoolean("%s") === %s (default = %s)',
(
testValue: boolean | string | number | undefined,
expectedResult: boolean,
_default: boolean,
) => {
expect(parseBoolean(testValue, _default)).toBe(expectedResult);
},
);

View File

@ -3,9 +3,12 @@ import * as log from 'loglevel';
import { isWindows } from '../helpers/helpers';
export function parseBoolean(
val: boolean | string | number,
val: boolean | string | number | undefined,
_default: boolean,
): boolean {
if (val === undefined) {
return _default;
}
try {
if (typeof val === 'boolean') {
return val;
@ -23,7 +26,7 @@ export function parseBoolean(
default:
return _default;
}
} catch (_) {
} catch {
return _default;
}
}
@ -39,11 +42,11 @@ export function parseBooleanOrString(val: string): boolean | string {
}
}
export function parseJson(val: string): any {
if (!val) return {};
export function parseJson<Type>(val: string): Type | undefined {
if (!val) return undefined;
try {
return JSON.parse(val);
} catch (err) {
return JSON.parse(val) as Type;
} catch (err: unknown) {
const windowsShellHint = isWindows()
? `\n In particular, Windows cmd doesn't have single quotes, so you have to use only double-quotes plus escaping: "{\\"someKey\\": \\"someValue\\"}"`
: '';

View File

@ -4,7 +4,7 @@ import sanitize = require('sanitize-filename');
import { DEFAULT_APP_NAME } from '../constants';
export function sanitizeFilename(
platform: string,
platform: string | undefined,
filenameToSanitize: string,
): string {
let result: string = sanitize(filenameToSanitize);

View File

@ -1,32 +1,36 @@
{
"compilerOptions": {
"allowJs": false,
"declaration": true,
"incremental": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./lib",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
// Bumping the minimum required Node version? You must bump:
// 1. package.json -> engines.node
// 2. package.json -> devDependencies.@types/node
// 3. tsconfig.json -> {target, lib}
// 4. .github/workflows/ci.yml -> node-version
//
// Here in tsconfig.json, we want to set the `target` and `lib` keys
// to the "best" values for the minimum/required version of node.
// TS doesn't offer any easy "preset" for this, so the best we have is to
// believe people who know which {syntax, library} parts of current EcmaScript
// are supported for our version of Node, and use what they recommend.
// For the current Node version, I followed
// https://stackoverflow.com/questions/59787574/typescript-tsconfig-settings-for-node-js-12
"target": "es2019",
// In `lib` we add `dom`, to tell tsc it's okay to use the URL object (which is in Node >= 7)
"lib": ["es2020", "dom"]
"allowJs": false,
"declaration": true,
"incremental": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./lib",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
// Bumping the minimum required Node version? You must bump:
// 1. package.json -> engines.node
// 2. package.json -> devDependencies.@types/node
// 3. tsconfig.json -> {target, lib}
// 4. .github/workflows/ci.yml -> node-version
//
// Here in tsconfig.json, we want to set the `target` and `lib` keys
// to the "best" values for the minimum/required version of node.
// TS doesn't offer any easy "preset" for this, so the best we have is to
// believe people who know which {syntax, library} parts of current EcmaScript
// are supported for our version of Node, and use what they recommend.
// For the current Node version, I followed
// https://stackoverflow.com/questions/59787574/typescript-tsconfig-settings-for-node-js-12
"target": "es2019",
// In `lib` we add `dom`, to tell tsc it's okay to use the URL object (which is in Node >= 7)
"lib": [
"es2020",
"dom"
]
},
"include": [
"./src/**/*"
"./src/**/*"
]
}