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: { rules: {
'no-console': 'error', 'no-console': 'error',
'prettier/prettier': [ 'prettier/prettier': [
'error', { 'error',
'endOfLine': 'auto' {
} endOfLine: 'auto',
},
], ],
// TODO remove when done killing `any`s and making tsc strict '@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-implicit-any-catch': 'error',
'@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-invalid-void-type': 'error',
'@typescript-eslint/no-unsafe-return': 'off', '@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 // https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files
ignorePatterns: [ ignorePatterns: [
"node_modules/**", 'node_modules/**',
"app/node_modules/**", 'app/node_modules/**',
"app/lib/**", 'app/lib/**',
"lib/**", 'lib/**',
"built-tests/**", 'built-tests/**',
"coverage/**", 'coverage/**',
] ],
}; };

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,8 @@ import * as path from 'path';
import { isOSX, isWindows, isLinux } from './helpers'; import { isOSX, isWindows, isLinux } from './helpers';
type fsError = Error & { code: string };
/** /**
* Find a file or directory * Find a file or directory
*/ */
@ -14,12 +16,12 @@ function findSync(
): string[] { ): string[] {
const matches: string[] = []; const matches: string[] = [];
(function findSyncRecurse(base) { (function findSyncRecurse(base): void {
let children: string[]; let children: string[];
try { try {
children = fs.readdirSync(base); children = fs.readdirSync(base);
} catch (err) { } catch (err: unknown) {
if (err.code === 'ENOENT') { if ((err as fsError).code === 'ENOENT') {
return; return;
} }
throw err; throw err;
@ -51,18 +53,18 @@ function findSync(
return matches; return matches;
} }
function findFlashOnLinux() { function findFlashOnLinux(): string {
return findSync(/libpepflashplayer\.so/, '/opt/google/chrome')[0]; return findSync(/libpepflashplayer\.so/, '/opt/google/chrome')[0];
} }
function findFlashOnWindows() { function findFlashOnWindows(): string {
return findSync( return findSync(
/pepflashplayer\.dll/, /pepflashplayer\.dll/,
'C:\\Program Files (x86)\\Google\\Chrome', 'C:\\Program Files (x86)\\Google\\Chrome',
)[0]; )[0];
} }
function findFlashOnMac() { function findFlashOnMac(): string {
return findSync( return findSync(
/PepperFlashPlayer.plugin/, /PepperFlashPlayer.plugin/,
'/Applications/Google Chrome.app/', '/Applications/Google Chrome.app/',
@ -70,7 +72,7 @@ function findFlashOnMac() {
)[0]; )[0];
} }
export function inferFlashPath() { export function inferFlashPath(): string {
if (isOSX()) { if (isOSX()) {
return findFlashOnMac(); 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 log from 'loglevel';
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers'; import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
@ -14,7 +20,7 @@ import {
export function onNewWindow( export function onNewWindow(
options, options,
setupWindow: (...args) => void, setupWindow: (...args) => void,
event: Event & { newGuest?: any }, event: NewWindowWebContentsEvent,
urlToGo: string, urlToGo: string,
frameName: string, frameName: string,
disposition: disposition:
@ -33,7 +39,7 @@ export function onNewWindow(
disposition, disposition,
parent, parent,
}); });
const preventDefault = (newGuest: any): void => { const preventDefault = (newGuest: BrowserWindow): void => {
event.preventDefault(); event.preventDefault();
if (newGuest) { if (newGuest) {
event.newGuest = newGuest; event.newGuest = newGuest;
@ -96,7 +102,7 @@ export function onNewWindowHelper(
} }
} }
return Promise.resolve(undefined); return Promise.resolve(undefined);
} catch (err) { } catch (err: unknown) {
return Promise.reject(err); return Promise.reject(err);
} }
} }
@ -153,7 +159,7 @@ export function setupNativefierWindow(options, window: BrowserWindow): void {
window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => { window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => {
onWillNavigate(options, event, url).catch((err) => { onWillNavigate(options, event, url).catch((err) => {
log.error(' window.webContents.on.will-navigate ERROR', err); log.error('window.webContents.on.will-navigate ERROR', err);
event.preventDefault(); event.preventDefault();
}); });
}); });
@ -161,14 +167,14 @@ export function setupNativefierWindow(options, window: BrowserWindow): void {
sendParamsOnDidFinishLoad(options, window); sendParamsOnDidFinishLoad(options, window);
// @ts-ignore new-tab isn't in the type definition, but it does exist // @ts-expect-error new-tab isn't in the type definition, but it does exist
window.on('new-tab', () => { window.on('new-tab', () =>
createNewTab( createNewTab(
options, options,
setupNativefierWindow, setupNativefierWindow,
options.targetUrl, options.targetUrl,
true, true,
window, window,
).catch((err) => log.error('new-tab ERROR', err)); ),
}); );
} }

View File

@ -140,7 +140,7 @@ describe('injectCSS', () => {
expect(mockGetCSSToInject).toHaveBeenCalled(); expect(mockGetCSSToInject).toHaveBeenCalled();
window.webContents.emit('did-navigate'); 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( window.webContents.session.webRequest.send(
'onHeadersReceived', 'onHeadersReceived',
{ responseHeaders, webContents: window.webContents }, { responseHeaders, webContents: window.webContents },
@ -167,7 +167,7 @@ describe('injectCSS', () => {
expect(mockGetCSSToInject).toHaveBeenCalled(); expect(mockGetCSSToInject).toHaveBeenCalled();
window.webContents.emit('did-navigate'); 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( window.webContents.session.webRequest.send(
'onHeadersReceived', 'onHeadersReceived',
{ responseHeaders, webContents: window.webContents }, { responseHeaders, webContents: window.webContents },
@ -202,7 +202,7 @@ describe('injectCSS', () => {
expect(window.webContents.emit('did-navigate')).toBe(true); expect(window.webContents.emit('did-navigate')).toBe(true);
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-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( window.webContents.session.webRequest.send(
'onHeadersReceived', 'onHeadersReceived',
{ {
@ -235,7 +235,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate'); window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); 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( window.webContents.session.webRequest.send(
'onHeadersReceived', 'onHeadersReceived',
{ {
@ -270,7 +270,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate'); window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); 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( window.webContents.session.webRequest.send(
'onHeadersReceived', 'onHeadersReceived',
{ {
@ -302,7 +302,7 @@ describe('injectCSS', () => {
window.webContents.emit('did-navigate'); window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); 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( window.webContents.session.webRequest.send(
'onHeadersReceived', 'onHeadersReceived',
{ {

View File

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

View File

@ -3,7 +3,7 @@ import 'source-map-support/register';
import fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { import electron, {
app, app,
crashReporter, crashReporter,
dialog, dialog,
@ -67,10 +67,11 @@ if (process.argv.length > 1) {
new URL(maybeUrl); new URL(maybeUrl);
appArgs.targetUrl = maybeUrl; appArgs.targetUrl = maybeUrl;
log.info('Loading override URL passed as argument:', maybeUrl); log.info('Loading override URL passed as argument:', maybeUrl);
} catch (err) { } catch (err: unknown) {
log.error( log.error(
'Not loading override URL passed as argument, because failed to parse:', 'Not loading override URL passed as argument, because failed to parse:',
maybeUrl, maybeUrl,
err,
); );
} }
} }
@ -155,12 +156,12 @@ if (appArgs.lang) {
let currentBadgeCount = 0; let currentBadgeCount = 0;
const setDockBadge = isOSX() const setDockBadge = isOSX()
? (count: number, bounce = false) => { ? (count: number, bounce = false): void => {
app.dock.setBadge(count.toString()); app.dock.setBadge(count.toString());
if (bounce && count > currentBadgeCount) app.dock.bounce(); if (bounce && count > currentBadgeCount) app.dock.bounce();
currentBadgeCount = count; currentBadgeCount = count;
} }
: () => undefined; : (): void => undefined;
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
log.debug('app.window-all-closed'); log.debug('app.window-all-closed');
@ -203,14 +204,14 @@ if (appArgs.crashReporter) {
} }
if (appArgs.widevine) { 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) => { app.on('widevine-ready', (version: string, lastVersion: string) => {
log.debug('app.widevine-ready', { version, lastVersion }); log.debug('app.widevine-ready', { version, lastVersion });
onReady().catch((err) => log.error('onReady ERROR', err)); onReady().catch((err) => log.error('onReady ERROR', err));
}); });
app.on( 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', 'widevine-update-pending',
(currentVersion: string, pendingVersion: string) => { (currentVersion: string, pendingVersion: string) => {
log.debug('app.widevine-update-pending', { 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 // @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
app.on('widevine-error', (error: any) => { app.on('widevine-error', (error: Error) => {
log.error('app.widevine-error', error); log.error('app.widevine-error', error);
}); });
} else { } 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 }); log.debug('app.activate', { event, hasVisibleWindows });
if (isOSX()) { if (isOSX()) {
// this is called when the dock is clicked // this is called when the dock is clicked
@ -374,7 +375,7 @@ app.on(
app.on( app.on(
'activity-was-continued', 'activity-was-continued',
(event: IpcMainEvent, type: string, userInfo: any) => { (event: IpcMainEvent, type: string, userInfo: unknown) => {
log.debug('app.activity-was-continued', { event, type, userInfo }); 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 */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
@ -21,12 +22,12 @@ import { EventEmitter } from 'events';
class MockBrowserWindow extends EventEmitter { class MockBrowserWindow extends EventEmitter {
webContents: MockWebContents; webContents: MockWebContents;
constructor(options?: any) { constructor(options?: unknown) {
super(options); super(options);
this.webContents = new MockWebContents(); this.webContents = new MockWebContents();
} }
addTabbedWindow(tab: MockBrowserWindow) { addTabbedWindow(tab: MockBrowserWindow): void {
return; return;
} }
@ -54,7 +55,7 @@ class MockBrowserWindow extends EventEmitter {
return undefined; return undefined;
} }
loadURL(url: string, options?: any): Promise<void> { loadURL(url: string, options?: unknown): Promise<void> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
@ -70,14 +71,14 @@ class MockBrowserWindow extends EventEmitter {
class MockDialog { class MockDialog {
static showMessageBox( static showMessageBox(
browserWindow: MockBrowserWindow, browserWindow: MockBrowserWindow,
options: any, options: unknown,
): Promise<number> { ): Promise<number> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
static showMessageBoxSync( static showMessageBoxSync(
browserWindow: MockBrowserWindow, browserWindow: MockBrowserWindow,
options: any, options: unknown,
): number { ): number {
return undefined; return undefined;
} }
@ -112,7 +113,7 @@ class MockWebContents extends EventEmitter {
return undefined; return undefined;
} }
insertCSS(css: string, options?: any): Promise<string> { insertCSS(css: string, options?: unknown): Promise<string> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
} }
@ -125,22 +126,24 @@ class MockWebRequest {
} }
onHeadersReceived( onHeadersReceived(
filter: any, filter: unknown,
listener: listener:
| (( | ((
details: any, details: unknown,
callback: (headersReceivedResponse: any) => void, callback: (headersReceivedResponse: unknown) => void,
) => void) ) => void)
| null, | null,
): void { ): void {
this.emitter.addListener( this.emitter.addListener(
'onHeadersReceived', '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); this.emitter.emit(event, ...args);
} }
} }

View File

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

View File

@ -37,8 +37,8 @@
"build:watch": "npm run clean && tsc --build . app --watch", "build:watch": "npm run clean && tsc --build . app --watch",
"changelog": "./.github/generate-changelog", "changelog": "./.github/generate-changelog",
"ci": "npm run lint && npm test", "ci": "npm run lint && npm test",
"clean": "rimraf lib/ app/lib/ app/dist/", "clean": "rimraf coverage/ lib/ app/lib/ app/dist/",
"clean:full": "rimraf lib/ app/lib/ app/dist/ app/node_modules/ node_modules/", "clean:full": "rimraf coverage/ lib/ app/lib/ app/dist/ app/node_modules/ node_modules/",
"lint:fix": "eslint . --ext .ts --fix", "lint:fix": "eslint . --ext .ts --fix",
"lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts'", "lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts'",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
@ -66,7 +66,10 @@
"yargs": "^17.0.1" "yargs": "^17.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/electron-packager": "^15.0.1",
"@types/hasbin": "^1.2.0",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"@types/loglevel": "^1.6.3",
"@types/ncp": "^2.0.4", "@types/ncp": "^2.0.4",
"@types/node": "^12.20.15", "@types/node": "^12.20.15",
"@types/page-icon": "^0.3.3", "@types/page-icon": "^0.3.3",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -65,9 +65,9 @@ function getExecutableArch(
function getExecutableInfo( function getExecutableInfo(
executablePath: string, executablePath: string,
platform: string, platform: string,
): ExecutableInfo { ): ExecutableInfo | undefined {
if (!fileExists(executablePath)) { if (!fileExists(executablePath)) {
return {}; return undefined;
} }
const exeBytes = getExecutableBytes(executablePath); const exeBytes = getExecutableBytes(executablePath);
@ -81,6 +81,12 @@ export function getOptionsFromExecutable(
priorOptions: NativefierOptions, priorOptions: NativefierOptions,
): NativefierOptions { ): NativefierOptions {
const newOptions: NativefierOptions = { ...priorOptions }; 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; let executablePath: string | undefined = undefined;
const appRoot = path.resolve(path.join(appResourcesDir, '..', '..')); const appRoot = path.resolve(path.join(appResourcesDir, '..', '..'));
@ -118,8 +124,7 @@ export function getOptionsFromExecutable(
appRoot, appRoot,
children.filter( children.filter(
(c) => (c) =>
c.name.toLowerCase() === `${newOptions.name.toLowerCase()}.exe` && c.name.toLowerCase() === `${name.toLowerCase()}.exe` && c.isFile(),
c.isFile(),
)[0].name, )[0].name,
); );
@ -130,7 +135,9 @@ export function getOptionsFromExecutable(
'ProductVersion', 'ProductVersion',
); );
log.debug( 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; newOptions.buildVersion = undefined;
} else { } else {
log.debug( 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', 'LegalCopyright',
); );
log.debug( log.debug(
`Extracted app copyright from executable: ${newOptions.appCopyright}`, `Extracted app copyright from executable: ${
newOptions.appCopyright as string
}`,
); );
} }
} else if (looksLikeLinux) { } else if (looksLikeLinux) {
@ -164,7 +175,13 @@ export function getOptionsFromExecutable(
} }
executablePath = path.join( executablePath = path.join(
appRoot, 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, executablePath,
newOptions.platform, newOptions.platform,
); );
if (!executableInfo) {
throw new Error(
`Could not get executable info for executable path: ${executablePath}`,
);
}
newOptions.arch = executableInfo.arch; 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) { if (newOptions.platform === undefined || newOptions.arch == undefined) {
throw Error(`Could not determine platform / arch of app in ${appRoot}`); 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( export function getVersionString(
executablePath: string, executablePath: string,
versionString: string, versionString: string,
): string { ): string | undefined {
let rcedit = path.resolve( let rcedit = path.resolve(
__dirname, __dirname,
'..', '..',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,13 +19,7 @@ export const supportedPlatforms = [
export function inferPlatform(): string { export function inferPlatform(): string {
const platform = os.platform(); const platform = os.platform();
if ( if (['darwin', 'linux', 'win32'].includes(platform)) {
platform === 'darwin' ||
// @ts-ignore
platform === 'mas' ||
platform === 'win32' ||
platform === 'linux'
) {
log.debug('Inferred platform', platform); log.debug('Inferred platform', platform);
return 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'; '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> { export async function inferTitle(url: string): Promise<string> {
const { data } = await axios.get(url, { const { data } = await axios.get<string>(url, {
headers: { headers: {
// Fake user agent for pages like http://messenger.com // Fake user agent for pages like http://messenger.com
'User-Agent': USER_AGENT, '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); log.debug(`Fetched ${(data.length / 1024).toFixed(1)} kb page at`, url);
const inferredTitle = 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'; 'Webapp';
log.debug('Inferred title:', inferredTitle); log.debug('Inferred title:', inferredTitle);
return inferredTitle; return inferredTitle;

View File

@ -10,9 +10,14 @@ import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion';
import { inferArch } from './infer/inferOs'; import { inferArch } from './infer/inferOs';
import { buildNativefierApp } from './main'; import { buildNativefierApp } from './main';
import { userAgent } from './options/fields/userAgent'; import { userAgent } from './options/fields/userAgent';
import { NativefierOptions } from './options/model';
import { parseJson } from './utils/parseUtils';
async function checkApp(appRoot: string, inputOptions: any): Promise<void> { async function checkApp(
const arch = (inputOptions.arch as string) || inferArch(); appRoot: string,
inputOptions: NativefierOptions,
): Promise<void> {
const arch = inputOptions.arch ? (inputOptions.arch as string) : inferArch();
if (inputOptions.out !== undefined) { if (inputOptions.out !== undefined) {
expect( expect(
path.join( path.join(
@ -43,28 +48,31 @@ async function checkApp(appRoot: string, inputOptions: any): Promise<void> {
const appPath = path.join(appRoot, relativeAppFolder); const appPath = path.join(appRoot, relativeAppFolder);
const configPath = path.join(appPath, 'nativefier.json'); const configPath = path.join(appPath, 'nativefier.json');
const nativefierConfig = JSON.parse(fs.readFileSync(configPath).toString()); const nativefierConfig: NativefierOptions | undefined =
expect(inputOptions.targetUrl).toBe(nativefierConfig.targetUrl); parseJson<NativefierOptions>(fs.readFileSync(configPath).toString());
expect(nativefierConfig).not.toBeUndefined();
expect(inputOptions.targetUrl).toBe(nativefierConfig?.targetUrl);
// Test name inferring // Test name inferring
expect(nativefierConfig.name).toBe('Google'); expect(nativefierConfig?.name).toBe('Google');
// Test icon writing // Test icon writing
const iconFile = const iconFile =
inputOptions.platform === 'darwin' ? '../electron.icns' : 'icon.png'; inputOptions.platform === 'darwin' ? '../electron.icns' : 'icon.png';
const iconPath = path.join(appPath, iconFile); 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); expect(fs.statSync(iconPath).size).toBeGreaterThan(1000);
// Test arch // Test arch
if (inputOptions.arch !== undefined) { if (inputOptions.arch !== undefined) {
expect(inputOptions.arch).toBe(nativefierConfig.arch); expect(inputOptions.arch).toEqual(nativefierConfig?.arch);
} else { } else {
expect(os.arch()).toBe(nativefierConfig.arch); expect(os.arch()).toEqual(nativefierConfig?.arch);
} }
// Test electron version // Test electron version
expect(nativefierConfig.electronVersionUsed).toBe( expect(nativefierConfig?.electronVersionUsed).toBe(
inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION, inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION,
); );
@ -80,10 +88,11 @@ async function checkApp(appRoot: string, inputOptions: any): Promise<void> {
}); });
inputOptions.userAgent = translatedUserAgent || inputOptions.userAgent; inputOptions.userAgent = translatedUserAgent || inputOptions.userAgent;
} }
expect(nativefierConfig.userAgent).toBe(inputOptions.userAgent);
expect(nativefierConfig?.userAgent).toEqual(inputOptions.userAgent);
// Test lang // Test lang
expect(nativefierConfig.lang).toBe(inputOptions.lang); expect(nativefierConfig?.lang).toEqual(inputOptions.lang);
} }
describe('Nativefier', () => { describe('Nativefier', () => {
@ -101,7 +110,8 @@ describe('Nativefier', () => {
lang: 'en-US', lang: 'en-US',
}; };
const appPath = await buildNativefierApp(options); 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, ...baseAppOptions,
}; };
const appPath = await buildNativefierApp(options); const appPath = await buildNativefierApp(options);
await checkApp(appPath, options); expect(appPath).not.toBeUndefined();
await checkApp(appPath as string, options);
const upgradeOptions = { const upgradeOptions = {
upgrade: appPath, upgrade: appPath,
@ -142,7 +153,8 @@ describe('Nativefier upgrade', () => {
const upgradeAppPath = await buildNativefierApp(upgradeOptions); const upgradeAppPath = await buildNativefierApp(upgradeOptions);
options.electronVersion = DEFAULT_ELECTRON_VERSION; options.electronVersion = DEFAULT_ELECTRON_VERSION;
options.userAgent = baseAppOptions.userAgent; 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 'source-map-support/register';
import { buildNativefierApp } from './build/buildNativefierApp'; import { buildNativefierApp } from './build/buildNativefierApp';
import { RawOptions } from './options/model';
export { buildNativefierApp }; export { buildNativefierApp };
@ -9,12 +10,12 @@ export { buildNativefierApp };
* Use the better, modern async `buildNativefierApp` instead if you can! * Use the better, modern async `buildNativefierApp` instead if you can!
*/ */
function buildNativefierAppOldCallbackStyle( function buildNativefierAppOldCallbackStyle(
options: any, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types options: RawOptions, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
callback: (err: any, result?: any) => void, callback: (err?: Error, result?: string) => void,
): void { ): void {
buildNativefierApp(options) buildNativefierApp(options)
.then((result) => callback(null, result)) .then((result) => callback(undefined, result))
.catch((err) => callback(err)); .catch((err: Error) => callback(err));
} }
export default buildNativefierAppOldCallbackStyle; export default buildNativefierAppOldCallbackStyle;

View File

@ -6,7 +6,7 @@ import { AppOptions } from './model';
/** /**
* Takes the options object and infers new values needing async work * 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.'); 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'; import { processOptions } from './fields';
describe('fields', () => {
let options: AppOptions;
test('fully-defined async options are returned as-is', async () => { beforeEach(() => {
const options = { options = {
packager: { nativefier: {
icon: '/my/icon.png', accessibilityPrompt: false,
name: 'my beautiful app ', alwaysOnTop: false,
targetUrl: 'https://myurl.com', backgroundColor: undefined,
dir: '/tmp/myapp', basicAuthPassword: undefined,
}, basicAuthUsername: undefined,
nativefier: { userAgent: 'random user agent' }, blockExternalUrls: false,
}; bookmarksMenu: undefined,
// @ts-ignore bounce: false,
await processOptions(options); 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'); test('fully-defined async options are returned as-is', async () => {
expect(options.packager.name).toEqual('my beautiful app'); options.packager.icon = '/my/icon.png';
expect(options.nativefier.userAgent).toEqual('random user agent'); options.packager.name = 'my beautiful app ';
}); options.packager.platform = 'darwin';
options.nativefier.userAgent = 'random user agent';
test('user agent is ignored if not provided', async () => { await processOptions(options);
const options = {
packager: { expect(options.packager.icon).toEqual('/my/icon.png');
icon: '/my/icon.png', expect(options.packager.name).toEqual('my beautiful app');
name: 'my beautiful app ', expect(options.nativefier.userAgent).toEqual('random user agent');
targetUrl: 'https://myurl.com', });
dir: '/tmp/myapp',
platform: 'linux', test('name has spaces stripped in linux', async () => {
}, options.packager.name = 'my beautiful app ';
nativefier: { userAgent: undefined }, options.packager.platform = 'linux';
}; await processOptions(options);
// @ts-ignore
await processOptions(options); expect(options.packager.name).toEqual('mybeautifulapp');
});
expect(options.nativefier.userAgent).toBeUndefined();
}); test('user agent is ignored if not provided', async () => {
await processOptions(options);
test('user agent short code is populated', async () => {
const options = { expect(options.nativefier.userAgent).toBeUndefined();
packager: { });
icon: '/my/icon.png',
name: 'my beautiful app ', test('user agent short code is populated', async () => {
targetUrl: 'https://myurl.com', options.nativefier.userAgent = 'edge';
dir: '/tmp/myapp', await processOptions(options);
platform: 'linux',
}, expect(options.nativefier.userAgent).not.toBe('edge');
nativefier: { userAgent: 'edge' }, });
};
// @ts-ignore
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 { AppOptions } from '../model';
import { name } from './name'; 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: 'nativefier', option: 'userAgent', processor: userAgent },
{ namespace: 'packager', option: 'icon', processor: icon }, { namespace: 'packager', option: 'icon', processor: icon },
{ namespace: 'packager', option: 'name', processor: name }, { 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( const processedOptions = await Promise.all(
OPTION_POSTPROCESSORS.map(async ({ namespace, option, processor }) => { OPTION_POSTPROCESSORS.map(async ({ namespace, option, processor }) => {
const result = await processor(options); const result = await processor(options);
@ -22,8 +28,15 @@ export async function processOptions(options: AppOptions): Promise<void> {
); );
for (const { namespace, option, result } of processedOptions) { 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; options[namespace][option] = result;
} }
} }
return options;
} }

View File

@ -24,7 +24,7 @@ const ICON_PARAMS_NEEDS_INFER = {
describe('when the icon parameter is passed', () => { describe('when the icon parameter is passed', () => {
test('it should return the icon parameter', async () => { test('it should return the icon parameter', async () => {
expect(inferIcon).toHaveBeenCalledTimes(0); 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); const result = await icon(ICON_PARAMS_NEEDS_INFER);
expect(result).toBe(null); expect(result).toBeUndefined();
expect(inferIcon).toHaveBeenCalledWith( expect(inferIcon).toHaveBeenCalledWith(
ICON_PARAMS_NEEDS_INFER.packager.targetUrl, ICON_PARAMS_NEEDS_INFER.packager.targetUrl,
ICON_PARAMS_NEEDS_INFER.packager.platform, 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) { if (options.packager.icon) {
log.debug('Got icon from options. Using it, no inferring needed'); 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 { try {
@ -21,11 +26,8 @@ export async function icon(options: IconParams): Promise<string> {
options.packager.targetUrl, options.packager.targetUrl,
options.packager.platform, options.packager.platform,
); );
} catch (error) { } catch (err: unknown) {
log.warn( log.warn('Cannot automatically retrieve the app icon:', err);
'Cannot automatically retrieve the app icon:', return undefined;
error.message || '',
);
return null;
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { CreateOptions } from 'asar';
import * as electronPackager from 'electron-packager'; import * as electronPackager from 'electron-packager';
export interface ElectronPackagerOptions extends electronPackager.Options { export interface ElectronPackagerOptions extends electronPackager.Options {
portable: boolean; portable: boolean;
platform: string; platform?: string;
targetUrl: string; targetUrl: string;
upgrade: boolean; upgrade: boolean;
upgradeFrom?: string; upgradeFrom?: string;
@ -13,60 +14,152 @@ export interface AppOptions {
nativefier: { nativefier: {
accessibilityPrompt: boolean; accessibilityPrompt: boolean;
alwaysOnTop: boolean; alwaysOnTop: boolean;
backgroundColor: string; backgroundColor?: string;
basicAuthPassword: string; basicAuthPassword?: string;
basicAuthUsername: string; basicAuthUsername?: string;
blockExternalUrls: boolean; blockExternalUrls: boolean;
bookmarksMenu: string; bookmarksMenu?: string;
bounce: boolean; bounce: boolean;
browserwindowOptions: any; browserwindowOptions?: BrowserWindowOptions;
clearCache: boolean; clearCache: boolean;
counter: boolean; counter: boolean;
crashReporter: string; crashReporter?: string;
disableContextMenu: boolean; disableContextMenu: boolean;
disableDevTools: boolean; disableDevTools: boolean;
disableGpu: boolean; disableGpu: boolean;
disableOldBuildWarning: boolean; disableOldBuildWarning: boolean;
diskCacheSize: number; diskCacheSize?: number;
electronVersionUsed?: string; electronVersionUsed?: string;
enableEs3Apis: boolean; enableEs3Apis: boolean;
fastQuit: boolean; fastQuit: boolean;
fileDownloadOptions: any; fileDownloadOptions: unknown;
flashPluginDir: string; flashPluginDir?: string;
fullScreen: boolean; fullScreen: boolean;
globalShortcuts: any; globalShortcuts?: GlobalShortcut[];
hideWindowFrame: boolean; hideWindowFrame: boolean;
ignoreCertificate: boolean; ignoreCertificate: boolean;
ignoreGpuBlacklist: boolean; ignoreGpuBlacklist: boolean;
inject: string[]; inject?: string[];
insecure: boolean; insecure: boolean;
internalUrls: string; internalUrls?: string;
lang?: string; lang?: string;
maximize: boolean; maximize: boolean;
nativefierVersion: string; nativefierVersion?: string;
processEnvs: string; processEnvs?: string;
proxyRules: string; proxyRules?: string;
showMenuBar: boolean; showMenuBar: boolean;
singleInstance: boolean; singleInstance: boolean;
titleBarStyle: string; titleBarStyle?: string;
tray: string | boolean; tray: string | boolean;
userAgent: string; userAgent?: string;
userAgentHonest: boolean; userAgentHonest: boolean;
verbose: boolean; verbose: boolean;
versionString: string; versionString?: string;
width: number; width?: number;
widevine: boolean; widevine: boolean;
height: number; height?: number;
minWidth: number; minWidth?: number;
minHeight: number; minHeight?: number;
maxWidth: number; maxWidth?: number;
maxHeight: number; maxHeight?: number;
x: number; x?: number;
y: number; y?: number;
zoom: number; zoom: number;
}; };
} }
export type BrowserWindowOptions = Record<string, unknown>;
export type GlobalShortcut = {
key: string;
};
export type NativefierOptions = Partial< export type NativefierOptions = Partial<
AppOptions['packager'] & AppOptions['nativefier'] 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; let parsedUrl: url.URL;
try { try {
parsedUrl = new url.URL(urlWithProtocol); parsedUrl = new url.URL(urlWithProtocol);
} catch (err) { } catch (err: unknown) {
throw `Your url "${urlWithProtocol}" is invalid`; log.error('normalizeUrl ERROR', err);
throw new Error(`Your url "${urlWithProtocol}" is invalid`);
} }
const normalizedUrl = parsedUrl.toString(); const normalizedUrl = parsedUrl.toString();
log.debug(`Normalized URL ${urlToNormalize} to:`, normalizedUrl); log.debug(`Normalized URL ${urlToNormalize} to:`, normalizedUrl);

View File

@ -1,9 +1,71 @@
import { getOptions, normalizePlatform } from './optionsMain'; import { getOptions, normalizePlatform } from './optionsMain';
import * as asyncConfig from './asyncConfig'; import * as asyncConfig from './asyncConfig';
import { inferPlatform } from '../infer/inferOs'; import { inferPlatform } from '../infer/inferOs';
import { AppOptions } from './model';
const mockedAsyncConfig = { some: 'options' };
let asyncConfigMock: jest.SpyInstance; 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(() => { beforeAll(() => {
asyncConfigMock = jest asyncConfigMock = jest
@ -18,8 +80,8 @@ test('it should call the async config', async () => {
const result = await getOptions(params); const result = await getOptions(params);
expect(asyncConfigMock).toHaveBeenCalledWith( expect(asyncConfigMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
packager: expect.anything(), packager: expect.anything() as AppOptions['packager'],
nativefier: expect.anything(), nativefier: expect.anything() as AppOptions['nativefier'],
}), }),
); );
expect(result.packager.targetUrl).toEqual(params.targetUrl); 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({ expect.objectContaining({
nativefier: expect.objectContaining({ nativefier: expect.objectContaining({
accessibilityPrompt: true, accessibilityPrompt: true,
}), }) as AppOptions['nativefier'],
}), }),
); );
expect(result.nativefier.accessibilityPrompt).toEqual(true); expect(result.nativefier.accessibilityPrompt).toEqual(true);

View File

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

View File

@ -14,8 +14,12 @@ test.each([
[undefined, true, true], [undefined, true, true],
[undefined, false, false], [undefined, false, false],
])( ])(
'parseBoolean("%s") === %s', 'parseBoolean("%s") === %s (default = %s)',
(testString: string, expectedResult: boolean, _default: boolean) => { (
expect(parseBoolean(testString, _default)).toBe(expectedResult); 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'; import { isWindows } from '../helpers/helpers';
export function parseBoolean( export function parseBoolean(
val: boolean | string | number, val: boolean | string | number | undefined,
_default: boolean, _default: boolean,
): boolean { ): boolean {
if (val === undefined) {
return _default;
}
try { try {
if (typeof val === 'boolean') { if (typeof val === 'boolean') {
return val; return val;
@ -23,7 +26,7 @@ export function parseBoolean(
default: default:
return _default; return _default;
} }
} catch (_) { } catch {
return _default; return _default;
} }
} }
@ -39,11 +42,11 @@ export function parseBooleanOrString(val: string): boolean | string {
} }
} }
export function parseJson(val: string): any { export function parseJson<Type>(val: string): Type | undefined {
if (!val) return {}; if (!val) return undefined;
try { try {
return JSON.parse(val); return JSON.parse(val) as Type;
} catch (err) { } catch (err: unknown) {
const windowsShellHint = isWindows() 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\\"}"` ? `\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'; import { DEFAULT_APP_NAME } from '../constants';
export function sanitizeFilename( export function sanitizeFilename(
platform: string, platform: string | undefined,
filenameToSanitize: string, filenameToSanitize: string,
): string { ): string {
let result: string = sanitize(filenameToSanitize); let result: string = sanitize(filenameToSanitize);

View File

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