From 7a08a2d6766bc068763013b632d2bc468ca1347f Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 15 Jun 2021 22:20:49 -0400 Subject: [PATCH] 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 --- .eslintrc.js | 38 ++--- .gitignore | 2 + app/src/components/mainWindow.ts | 27 ++-- app/src/components/menu.ts | 23 ++- app/src/components/trayIcon.ts | 2 +- app/src/helpers/helpers.ts | 2 +- app/src/helpers/inferFlash.ts | 16 ++- app/src/helpers/windowEvents.ts | 24 ++-- app/src/helpers/windowHelpers.test.ts | 12 +- app/src/helpers/windowHelpers.ts | 4 +- app/src/main.ts | 21 +-- app/src/mocks/electron.ts | 27 ++-- app/src/preload.ts | 28 ++-- package.json | 7 +- src/build/buildIcon.ts | 12 +- src/build/buildNativefierApp.ts | 43 +++--- src/build/prepareElectronApp.ts | 31 ++-- src/cli.test.ts | 164 ++++++++++++---------- src/cli.ts | 36 ++--- src/helpers/helpers.ts | 26 ++-- src/helpers/upgrade/executableHelpers.ts | 41 ++++-- src/helpers/upgrade/rceditGet.ts | 2 +- src/helpers/upgrade/upgrade.ts | 29 ++-- src/infer/browsers/inferChromeVersion.ts | 4 +- src/infer/browsers/inferFirefoxVersion.ts | 4 +- src/infer/browsers/inferSafariVersion.ts | 21 +-- src/infer/inferIcon.ts | 62 ++++---- src/infer/inferOs.ts | 8 +- src/infer/inferTitle.ts | 4 +- src/integration-test.ts | 40 ++++-- src/main.ts | 9 +- src/options/asyncConfig.ts | 4 +- src/options/fields/fields.test.ts | 148 ++++++++++++------- src/options/fields/fields.ts | 19 ++- src/options/fields/icon.test.ts | 4 +- src/options/fields/icon.ts | 18 +-- src/options/fields/name.ts | 18 +-- src/options/fields/userAgent.test.ts | 4 +- src/options/fields/userAgent.ts | 25 ++-- src/options/model.ts | 147 +++++++++++++++---- src/options/normalizeUrl.ts | 5 +- src/options/optionsMain.test.ts | 70 ++++++++- src/options/optionsMain.ts | 145 +++++++++++-------- src/utils/parseUtils.test.ts | 10 +- src/utils/parseUtils.ts | 15 +- src/utils/sanitizeFilename.ts | 2 +- tsconfig.json | 56 ++++---- 47 files changed, 919 insertions(+), 540 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 1a41851..5ff49dc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,25 +17,29 @@ module.exports = { rules: { 'no-console': 'error', 'prettier/prettier': [ - 'error', { - 'endOfLine': 'auto' - } + 'error', + { + endOfLine: 'auto', + }, ], - // TODO remove when done killing `any`s and making tsc strict - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-confusing-non-null-assertion': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-implicit-any-catch': 'error', + '@typescript-eslint/no-invalid-void-type': 'error', + '@typescript-eslint/prefer-ts-expect-error': 'error', + '@typescript-eslint/type-annotation-spacing': 'error', + '@typescript-eslint/typedef': 'error', + '@typescript-eslint/unified-signatures': 'error', }, // https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files ignorePatterns: [ - "node_modules/**", - "app/node_modules/**", - "app/lib/**", - "lib/**", - "built-tests/**", - "coverage/**", - ] + 'node_modules/**', + 'app/node_modules/**', + 'app/lib/**', + 'lib/**', + 'built-tests/**', + 'coverage/**', + ], }; diff --git a/.gitignore b/.gitignore index 06ef34d..4423e52 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ nativefier*.tgz # https://github.com/nektos/act .actrc + +tsconfig.tsbuildinfo diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 7866104..5ef08c0 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -25,14 +25,14 @@ export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json'); type SessionInteractionRequest = { id?: string; func?: string; - funcArgs?: any[]; + funcArgs?: unknown[]; property?: string; - propertyValue?: any; + propertyValue?: unknown; }; type SessionInteractionResult = { id?: string; - value?: any; + value?: unknown; error?: Error; }; @@ -144,20 +144,17 @@ function createContextMenu(options, window: BrowserWindow): void { } } -export function saveAppArgs(newAppArgs: any) { +export function saveAppArgs(newAppArgs: any): void { try { fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs)); - } catch (err) { - // eslint-disable-next-line no-console + } catch (err: unknown) { log.warn( - `WARNING: Ignored nativefier.json rewrital (${( - err as Error - ).toString()})`, + `WARNING: Ignored nativefier.json rewrital (${(err as Error).message})`, ); } } -function setupCloseEvent(options, window: BrowserWindow) { +function setupCloseEvent(options, window: BrowserWindow): void { window.on('close', (event: IpcMainEvent) => { log.debug('mainWindow.close', event); if (window.isFullScreen()) { @@ -181,7 +178,7 @@ function setupCounter( options, window: BrowserWindow, setDockBadge: (value: number | string, bounce?: boolean) => void, -) { +): void { window.on('page-title-updated', (event, title) => { log.debug('mainWindow.page-title-updated', { event, title }); const counterValue = getCounterValue(title); @@ -242,7 +239,7 @@ function setupSessionInteraction(options, window: BrowserWindow): void { typeof result.value['then'] === 'function' ) { // This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply - result.value + (result.value as Promise) .then((trueResultValue) => { result.value = trueResultValue; log.debug('ipcMain.session-interaction:result', result); @@ -274,9 +271,9 @@ function setupSessionInteraction(options, window: BrowserWindow): void { log.debug('session-interaction:result', result); event.reply('session-interaction-reply', result); } - } catch (error) { - log.error('session-interaction:error', error, event, request); - result.error = error; + } catch (err: unknown) { + log.error('session-interaction:error', err, event, request); + result.error = err as Error; result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place event.reply('session-interaction-reply', result); } diff --git a/app/src/components/menu.ts b/app/src/components/menu.ts index ed761ee..bfac635 100644 --- a/app/src/components/menu.ts +++ b/app/src/components/menu.ts @@ -89,9 +89,7 @@ export function generateMenu( { label: 'Copy Current URL', accelerator: 'CmdOrCtrl+L', - click: () => { - clipboard.writeText(getCurrentURL()); - }, + click: (): void => clipboard.writeText(getCurrentURL()), }, { label: 'Paste', @@ -110,7 +108,7 @@ export function generateMenu( }, { label: 'Clear App Data', - click: (item: MenuItem, focusedWindow: BrowserWindow) => { + click: (item: MenuItem, focusedWindow: BrowserWindow): void => { log.debug('Clear App Data.click', { item, focusedWindow, @@ -166,7 +164,7 @@ export function generateMenu( accelerator: isOSX() ? 'Ctrl+Cmd+F' : 'F11', enabled: mainWindow.isFullScreenable() || isOSX(), visible: mainWindow.isFullScreenable() || isOSX(), - click: (item: MenuItem, focusedWindow: BrowserWindow) => { + click: (item: MenuItem, focusedWindow: BrowserWindow): void => { log.debug('Toggle Full Screen.click()', { item, focusedWindow, @@ -266,9 +264,9 @@ export function generateMenu( submenu: [ { label: `Built with Nativefier v${nativefierVersion}`, - click: () => { + click: (): void => { openExternal('https://github.com/nativefier/nativefier').catch( - (err) => + (err: unknown): void => log.error( 'Built with Nativefier v${nativefierVersion}.click ERROR', err, @@ -278,9 +276,10 @@ export function generateMenu( }, { label: 'Report an Issue', - click: () => { + click: (): void => { openExternal('https://github.com/nativefier/nativefier/issues').catch( - (err) => log.error('Report an Issue.click ERROR', err), + (err: unknown): void => + log.error('Report an Issue.click ERROR', err), ); }, }, @@ -370,8 +369,8 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void { } return { label: bookmark.title, - click: () => { - goToURL(bookmark.url).catch((err) => + click: (): void => { + goToURL(bookmark.url).catch((err: unknown): void => log.error(`${bookmark.title}.click ERROR`, err), ); }, @@ -390,7 +389,7 @@ function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void { }; // Insert custom bookmarks menu between menus "View" and "Window" menuTemplate.splice(menuTemplate.length - 2, 0, bookmarksMenu); - } catch (err) { + } catch (err: unknown) { log.error('Failed to load & parse bookmarks configuration JSON file.', err); } } diff --git a/app/src/components/trayIcon.ts b/app/src/components/trayIcon.ts index 6d4b80b..59d7790 100644 --- a/app/src/components/trayIcon.ts +++ b/app/src/components/trayIcon.ts @@ -23,7 +23,7 @@ export function createTrayIcon( appIcon.setImage(nimage); } - const onClick = () => { + const onClick = (): void => { log.debug('onClick'); if (mainWindow.isVisible()) { mainWindow.hide(); diff --git a/app/src/helpers/helpers.ts b/app/src/helpers/helpers.ts index 9690109..47e5538 100644 --- a/app/src/helpers/helpers.ts +++ b/app/src/helpers/helpers.ts @@ -141,7 +141,7 @@ export function linkIsInternal( // Only use the tld and the main domain for domain-ish test // Enables domain-ish equality for blog.foo.com and shop.foo.com return domainify(currentUrl) === domainify(newUrl); - } catch (err) { + } catch (err: unknown) { log.error( 'Failed to parse domains as determining if link is internal. From:', currentUrl, diff --git a/app/src/helpers/inferFlash.ts b/app/src/helpers/inferFlash.ts index 1603f8a..6c61bd3 100644 --- a/app/src/helpers/inferFlash.ts +++ b/app/src/helpers/inferFlash.ts @@ -4,6 +4,8 @@ import * as path from 'path'; import { isOSX, isWindows, isLinux } from './helpers'; +type fsError = Error & { code: string }; + /** * Find a file or directory */ @@ -14,12 +16,12 @@ function findSync( ): string[] { const matches: string[] = []; - (function findSyncRecurse(base) { + (function findSyncRecurse(base): void { let children: string[]; try { children = fs.readdirSync(base); - } catch (err) { - if (err.code === 'ENOENT') { + } catch (err: unknown) { + if ((err as fsError).code === 'ENOENT') { return; } throw err; @@ -51,18 +53,18 @@ function findSync( return matches; } -function findFlashOnLinux() { +function findFlashOnLinux(): string { return findSync(/libpepflashplayer\.so/, '/opt/google/chrome')[0]; } -function findFlashOnWindows() { +function findFlashOnWindows(): string { return findSync( /pepflashplayer\.dll/, 'C:\\Program Files (x86)\\Google\\Chrome', )[0]; } -function findFlashOnMac() { +function findFlashOnMac(): string { return findSync( /PepperFlashPlayer.plugin/, '/Applications/Google Chrome.app/', @@ -70,7 +72,7 @@ function findFlashOnMac() { )[0]; } -export function inferFlashPath() { +export function inferFlashPath(): string { if (isOSX()) { return findFlashOnMac(); } diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts index fc4ab9e..b5274cb 100644 --- a/app/src/helpers/windowEvents.ts +++ b/app/src/helpers/windowEvents.ts @@ -1,4 +1,10 @@ -import { dialog, BrowserWindow, IpcMainEvent, WebContents } from 'electron'; +import { + dialog, + BrowserWindow, + IpcMainEvent, + NewWindowWebContentsEvent, + WebContents, +} from 'electron'; import log from 'loglevel'; import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers'; @@ -14,7 +20,7 @@ import { export function onNewWindow( options, setupWindow: (...args) => void, - event: Event & { newGuest?: any }, + event: NewWindowWebContentsEvent, urlToGo: string, frameName: string, disposition: @@ -33,7 +39,7 @@ export function onNewWindow( disposition, parent, }); - const preventDefault = (newGuest: any): void => { + const preventDefault = (newGuest: BrowserWindow): void => { event.preventDefault(); if (newGuest) { event.newGuest = newGuest; @@ -96,7 +102,7 @@ export function onNewWindowHelper( } } return Promise.resolve(undefined); - } catch (err) { + } catch (err: unknown) { return Promise.reject(err); } } @@ -153,7 +159,7 @@ export function setupNativefierWindow(options, window: BrowserWindow): void { window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => { onWillNavigate(options, event, url).catch((err) => { - log.error(' window.webContents.on.will-navigate ERROR', err); + log.error('window.webContents.on.will-navigate ERROR', err); event.preventDefault(); }); }); @@ -161,14 +167,14 @@ export function setupNativefierWindow(options, window: BrowserWindow): void { sendParamsOnDidFinishLoad(options, window); - // @ts-ignore new-tab isn't in the type definition, but it does exist - window.on('new-tab', () => { + // @ts-expect-error new-tab isn't in the type definition, but it does exist + window.on('new-tab', () => createNewTab( options, setupNativefierWindow, options.targetUrl, true, window, - ).catch((err) => log.error('new-tab ERROR', err)); - }); + ), + ); } diff --git a/app/src/helpers/windowHelpers.test.ts b/app/src/helpers/windowHelpers.test.ts index 0ec7327..e6d9591 100644 --- a/app/src/helpers/windowHelpers.test.ts +++ b/app/src/helpers/windowHelpers.test.ts @@ -140,7 +140,7 @@ describe('injectCSS', () => { expect(mockGetCSSToInject).toHaveBeenCalled(); window.webContents.emit('did-navigate'); - // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock window.webContents.session.webRequest.send( 'onHeadersReceived', { responseHeaders, webContents: window.webContents }, @@ -167,7 +167,7 @@ describe('injectCSS', () => { expect(mockGetCSSToInject).toHaveBeenCalled(); window.webContents.emit('did-navigate'); - // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock window.webContents.session.webRequest.send( 'onHeadersReceived', { responseHeaders, webContents: window.webContents }, @@ -202,7 +202,7 @@ describe('injectCSS', () => { expect(window.webContents.emit('did-navigate')).toBe(true); mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); - // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock window.webContents.session.webRequest.send( 'onHeadersReceived', { @@ -235,7 +235,7 @@ describe('injectCSS', () => { window.webContents.emit('did-navigate'); mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); - // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock window.webContents.session.webRequest.send( 'onHeadersReceived', { @@ -270,7 +270,7 @@ describe('injectCSS', () => { window.webContents.emit('did-navigate'); mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); - // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock window.webContents.session.webRequest.send( 'onHeadersReceived', { @@ -302,7 +302,7 @@ describe('injectCSS', () => { window.webContents.emit('did-navigate'); mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); - // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock window.webContents.session.webRequest.send( 'onHeadersReceived', { diff --git a/app/src/helpers/windowHelpers.ts b/app/src/helpers/windowHelpers.ts index b0eeefd..d8193d5 100644 --- a/app/src/helpers/windowHelpers.ts +++ b/app/src/helpers/windowHelpers.ts @@ -83,7 +83,7 @@ export function createNewTab( url: string, foreground: boolean, parent?: BrowserWindow, -): Promise { +): BrowserWindow { log.debug('createNewTab', { url, foreground, parent }); return withFocusedWindow((focusedWindow) => { const newTab = createNewWindow(options, setupWindow, url, parent); @@ -314,7 +314,7 @@ export function setProxyRules(window: BrowserWindow, proxyRules): void { .catch((err) => log.error('session.setProxy ERROR', err)); } -export function withFocusedWindow(block: (window: BrowserWindow) => any): any { +export function withFocusedWindow(block: (window: BrowserWindow) => T): T { const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) { return block(focusedWindow); diff --git a/app/src/main.ts b/app/src/main.ts index e33c010..8a798bc 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -3,7 +3,7 @@ import 'source-map-support/register'; import fs from 'fs'; import * as path from 'path'; -import { +import electron, { app, crashReporter, dialog, @@ -67,10 +67,11 @@ if (process.argv.length > 1) { new URL(maybeUrl); appArgs.targetUrl = maybeUrl; log.info('Loading override URL passed as argument:', maybeUrl); - } catch (err) { + } catch (err: unknown) { log.error( 'Not loading override URL passed as argument, because failed to parse:', maybeUrl, + err, ); } } @@ -155,12 +156,12 @@ if (appArgs.lang) { let currentBadgeCount = 0; const setDockBadge = isOSX() - ? (count: number, bounce = false) => { + ? (count: number, bounce = false): void => { app.dock.setBadge(count.toString()); if (bounce && count > currentBadgeCount) app.dock.bounce(); currentBadgeCount = count; } - : () => undefined; + : (): void => undefined; app.on('window-all-closed', () => { log.debug('app.window-all-closed'); @@ -203,14 +204,14 @@ if (appArgs.crashReporter) { } if (appArgs.widevine) { - // @ts-ignore This event only appears on the widevine version of electron, which we'd see at runtime + // @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime app.on('widevine-ready', (version: string, lastVersion: string) => { log.debug('app.widevine-ready', { version, lastVersion }); onReady().catch((err) => log.error('onReady ERROR', err)); }); app.on( - // @ts-ignore This event only appears on the widevine version of electron, which we'd see at runtime + // @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime 'widevine-update-pending', (currentVersion: string, pendingVersion: string) => { log.debug('app.widevine-update-pending', { @@ -220,8 +221,8 @@ if (appArgs.widevine) { }, ); - // @ts-ignore This event only appears on the widevine version of electron, which we'd see at runtime - app.on('widevine-error', (error: any) => { + // @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime + app.on('widevine-error', (error: Error) => { log.error('app.widevine-error', error); }); } else { @@ -231,7 +232,7 @@ if (appArgs.widevine) { }); } -app.on('activate', (event, hasVisibleWindows) => { +app.on('activate', (event: electron.Event, hasVisibleWindows: boolean) => { log.debug('app.activate', { event, hasVisibleWindows }); if (isOSX()) { // this is called when the dock is clicked @@ -374,7 +375,7 @@ app.on( app.on( 'activity-was-continued', - (event: IpcMainEvent, type: string, userInfo: any) => { + (event: IpcMainEvent, type: string, userInfo: unknown) => { log.debug('app.activity-was-continued', { event, type, userInfo }); }, ); diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts index 1e451df..720fd28 100644 --- a/app/src/mocks/electron.ts +++ b/app/src/mocks/electron.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ /* eslint-disable @typescript-eslint/no-unused-vars */ import { EventEmitter } from 'events'; @@ -21,12 +22,12 @@ import { EventEmitter } from 'events'; class MockBrowserWindow extends EventEmitter { webContents: MockWebContents; - constructor(options?: any) { + constructor(options?: unknown) { super(options); this.webContents = new MockWebContents(); } - addTabbedWindow(tab: MockBrowserWindow) { + addTabbedWindow(tab: MockBrowserWindow): void { return; } @@ -54,7 +55,7 @@ class MockBrowserWindow extends EventEmitter { return undefined; } - loadURL(url: string, options?: any): Promise { + loadURL(url: string, options?: unknown): Promise { return Promise.resolve(undefined); } @@ -70,14 +71,14 @@ class MockBrowserWindow extends EventEmitter { class MockDialog { static showMessageBox( browserWindow: MockBrowserWindow, - options: any, + options: unknown, ): Promise { return Promise.resolve(undefined); } static showMessageBoxSync( browserWindow: MockBrowserWindow, - options: any, + options: unknown, ): number { return undefined; } @@ -112,7 +113,7 @@ class MockWebContents extends EventEmitter { return undefined; } - insertCSS(css: string, options?: any): Promise { + insertCSS(css: string, options?: unknown): Promise { return Promise.resolve(undefined); } } @@ -125,22 +126,24 @@ class MockWebRequest { } onHeadersReceived( - filter: any, + filter: unknown, listener: | (( - details: any, - callback: (headersReceivedResponse: any) => void, + details: unknown, + callback: (headersReceivedResponse: unknown) => void, ) => void) | null, ): void { this.emitter.addListener( 'onHeadersReceived', - (details: any, callback: (headersReceivedResponse: any) => void) => - listener(details, callback), + ( + details: unknown, + callback: (headersReceivedResponse: unknown) => void, + ) => listener(details, callback), ); } - send(event: string, ...args: any[]): void { + send(event: string, ...args: unknown[]): void { this.emitter.emit(event, ...args); } } diff --git a/app/src/preload.ts b/app/src/preload.ts index 60ac2da..613f54e 100644 --- a/app/src/preload.ts +++ b/app/src/preload.ts @@ -34,9 +34,18 @@ export const INJECT_DIR = path.join(__dirname, '..', 'inject'); * @param createCallback * @param clickCallback */ -function setNotificationCallback(createCallback, clickCallback) { +function setNotificationCallback( + createCallback: { + (title: string, opt: NotificationOptions): void; + (...args: unknown[]): void; + }, + clickCallback: { (): void; (this: Notification, ev: Event): unknown }, +): void { const OldNotify = window.Notification; - const newNotify = function (title, opt) { + const newNotify = function ( + title: string, + opt: NotificationOptions, + ): Notification { createCallback(title, opt); const instance = new OldNotify(title, opt); instance.addEventListener('click', clickCallback); @@ -47,11 +56,11 @@ function setNotificationCallback(createCallback, clickCallback) { get: () => OldNotify.permission, }); - // @ts-ignore + // @ts-expect-error window.Notification = newNotify; } -function injectScripts() { +function injectScripts(): void { const needToInject = fs.existsSync(INJECT_DIR); if (!needToInject) { return; @@ -68,15 +77,18 @@ function injectScripts() { log.debug('Injecting JS file', jsFile); require(jsFile); } - } catch (error) { - log.error('Error encoutered injecting JS files', error); + } catch (err: unknown) { + log.error('Error encoutered injecting JS files', err); } } -function notifyNotificationCreate(title, opt) { +function notifyNotificationCreate( + title: string, + opt: NotificationOptions, +): void { ipcRenderer.send('notification', title, opt); } -function notifyNotificationClick() { +function notifyNotificationClick(): void { ipcRenderer.send('notification-click'); } diff --git a/package.json b/package.json index 4048e46..8eaa0f3 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "build:watch": "npm run clean && tsc --build . app --watch", "changelog": "./.github/generate-changelog", "ci": "npm run lint && npm test", - "clean": "rimraf lib/ app/lib/ app/dist/", - "clean:full": "rimraf lib/ app/lib/ app/dist/ app/node_modules/ node_modules/", + "clean": "rimraf coverage/ lib/ app/lib/ app/dist/", + "clean:full": "rimraf coverage/ lib/ app/lib/ app/dist/ app/node_modules/ node_modules/", "lint:fix": "eslint . --ext .ts --fix", "lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts'", "lint": "eslint . --ext .ts", @@ -66,7 +66,10 @@ "yargs": "^17.0.1" }, "devDependencies": { + "@types/electron-packager": "^15.0.1", + "@types/hasbin": "^1.2.0", "@types/jest": "^26.0.23", + "@types/loglevel": "^1.6.3", "@types/ncp": "^2.0.4", "@types/node": "^12.20.15", "@types/page-icon": "^0.3.3", diff --git a/src/build/buildIcon.ts b/src/build/buildIcon.ts index f45570d..ad99c4c 100644 --- a/src/build/buildIcon.ts +++ b/src/build/buildIcon.ts @@ -45,8 +45,8 @@ export function convertIconIfNecessary(options: AppOptions): void { const iconPath = convertToIco(options.packager.icon); options.packager.icon = iconPath; return; - } catch (error) { - log.warn('Failed to convert icon to .ico, skipping.', error); + } catch (err: unknown) { + log.warn('Failed to convert icon to .ico, skipping.', err); return; } } @@ -63,8 +63,8 @@ export function convertIconIfNecessary(options: AppOptions): void { const iconPath = convertToPng(options.packager.icon); options.packager.icon = iconPath; return; - } catch (error) { - log.warn('Failed to convert icon to .png, skipping.', error); + } catch (err: unknown) { + log.warn('Failed to convert icon to .png, skipping.', err); return; } } @@ -90,8 +90,8 @@ export function convertIconIfNecessary(options: AppOptions): void { if (options.nativefier.tray) { convertToTrayIcon(options.packager.icon); } - } catch (error) { - log.warn('Failed to convert icon to .icns, skipping.', error); + } catch (err: unknown) { + log.warn('Failed to convert icon to .icns, skipping.', err); options.packager.icon = undefined; } } diff --git a/src/build/buildNativefierApp.ts b/src/build/buildNativefierApp.ts index a531d58..dc40b14 100644 --- a/src/build/buildNativefierApp.ts +++ b/src/build/buildNativefierApp.ts @@ -2,18 +2,18 @@ import * as path from 'path'; import * as electronGet from '@electron/get'; import * as electronPackager from 'electron-packager'; -import * as hasbin from 'hasbin'; import * as log from 'loglevel'; import { convertIconIfNecessary } from './buildIcon'; import { copyFileOrDir, getTempDir, + hasWine, isWindows, isWindowsAdmin, } from '../helpers/helpers'; import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade'; -import { AppOptions } from '../options/model'; +import { AppOptions, RawOptions } from '../options/model'; import { getOptions } from '../options/optionsMain'; import { prepareElectronApp } from './prepareElectronApp'; @@ -70,13 +70,13 @@ async function copyIconsIfNecessary( /** * Checks the app path array to determine if packaging completed successfully */ -function getAppPath(appPath: string | string[]): string { +function getAppPath(appPath: string | string[]): string | undefined { if (!Array.isArray(appPath)) { return appPath; } if (appPath.length === 0) { - return null; // directory already exists and `--overwrite` not set + return undefined; // directory already exists and `--overwrite` not set } if (appPath.length > 1) { @@ -89,20 +89,21 @@ function getAppPath(appPath: string | string[]): string { return appPath[0]; } -function isUpgrade(rawOptions) { - return ( +function isUpgrade(rawOptions: RawOptions): boolean { + if ( rawOptions.upgrade !== undefined && - (rawOptions.upgrade === true || - (typeof rawOptions.upgrade === 'string' && rawOptions.upgrade !== '')) - ); + typeof rawOptions.upgrade === 'string' && + rawOptions.upgrade !== '' + ) { + rawOptions.upgradeFrom = rawOptions.upgrade; + rawOptions.upgrade = true; + return true; + } + return false; } function trimUnprocessableOptions(options: AppOptions): void { - if ( - options.packager.platform === 'win32' && - !isWindows() && - !hasbin.sync('wine') - ) { + if (options.packager.platform === 'win32' && !isWindows() && !hasWine()) { const optionsPresent = Object.entries(options) .filter( ([key, value]) => @@ -119,28 +120,30 @@ function trimUnprocessableOptions(options: AppOptions): void { 'features, like a correct icon and process name. Do yourself a favor and install Wine, please.', ); for (const keyToUnset of optionsPresent) { - options[keyToUnset] = null; + (options as unknown as Record)[keyToUnset] = undefined; } } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export async function buildNativefierApp(rawOptions): Promise { +export async function buildNativefierApp( + rawOptions: RawOptions, +): Promise { log.info('\nProcessing options...'); if (isUpgrade(rawOptions)) { - log.debug('Attempting to upgrade from', rawOptions.upgrade); - const oldApp = findUpgradeApp(rawOptions.upgrade.toString()); + log.debug('Attempting to upgrade from', rawOptions.upgradeFrom); + const oldApp = findUpgradeApp(rawOptions.upgradeFrom as string); if (oldApp === null) { throw new Error( `Could not find an old Nativfier app in "${ - rawOptions.upgrade as string + rawOptions.upgradeFrom as string }"`, ); } rawOptions = useOldAppOptions(rawOptions, oldApp); if (rawOptions.out === undefined && rawOptions.overwrite) { - rawOptions.out = path.dirname(rawOptions.upgrade); + rawOptions.out = path.dirname(rawOptions.upgradeFrom as string); } } log.debug('rawOptions', rawOptions); diff --git a/src/build/prepareElectronApp.ts b/src/build/prepareElectronApp.ts index eda7274..c40baf0 100644 --- a/src/build/prepareElectronApp.ts +++ b/src/build/prepareElectronApp.ts @@ -6,14 +6,15 @@ import { promisify } from 'util'; import * as log from 'loglevel'; import { copyFileOrDir, generateRandomSuffix } from '../helpers/helpers'; -import { AppOptions } from '../options/model'; +import { AppOptions, OutputOptions, PackageJSON } from '../options/model'; +import { parseJson } from '../utils/parseUtils'; const writeFileAsync = promisify(fs.writeFile); /** * Only picks certain app args to pass to nativefier.json */ -function pickElectronAppArgs(options: AppOptions): any { +function pickElectronAppArgs(options: AppOptions): OutputOptions { return { accessibilityPrompt: options.nativefier.accessibilityPrompt, alwaysOnTop: options.nativefier.alwaysOnTop, @@ -99,7 +100,10 @@ function pickElectronAppArgs(options: AppOptions): any { }; } -async function maybeCopyScripts(srcs: string[], dest: string): Promise { +async function maybeCopyScripts( + srcs: string[] | undefined, + dest: string, +): Promise { if (!srcs || srcs.length === 0) { log.debug('No files to inject, skipping copy.'); return; @@ -151,7 +155,12 @@ function changeAppPackageJsonName( url: string, ): void { const packageJsonPath = path.join(appPath, '/package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); + const packageJson = parseJson( + fs.readFileSync(packageJsonPath).toString(), + ); + if (!packageJson) { + throw new Error(`Could not load package.json from ${packageJsonPath}`); + } const normalizedAppName = normalizeAppName(name, url); packageJson.name = normalizedAppName; log.debug(`Updating ${packageJsonPath} 'name' field to ${normalizedAppName}`); @@ -171,10 +180,10 @@ export async function prepareElectronApp( log.debug(`Copying electron app from ${src} to ${dest}`); try { await copyFileOrDir(src, dest); - } catch (err) { - throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${( - err as Error - ).toString()}`; + } catch (err: unknown) { + throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${ + (err as Error).message + }`; } const appJsonPath = path.join(dest, '/nativefier.json'); @@ -188,19 +197,19 @@ export async function prepareElectronApp( const bookmarksJsonPath = path.join(dest, '/bookmarks.json'); try { await copyFileOrDir(options.nativefier.bookmarksMenu, bookmarksJsonPath); - } catch (err) { + } catch (err: unknown) { log.error('Error copying bookmarks menu config file.', err); } } try { await maybeCopyScripts(options.nativefier.inject, dest); - } catch (err) { + } catch (err: unknown) { log.error('Error copying injection files.', err); } changeAppPackageJsonName( dest, - options.packager.name, + options.packager.name as string, options.packager.targetUrl, ); } diff --git a/src/cli.test.ts b/src/cli.test.ts index 5630e06..c4b9693 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -51,22 +51,22 @@ describe('initArgs + parseArgs', () => { test('upgrade arg', () => { const args = parseArgs(initArgs(['--upgrade', 'pathToUpgrade'])); expect(args.upgrade).toBe('pathToUpgrade'); - expect(args.targetUrl).toBe(''); + expect(args.targetUrl).toBeUndefined(); }); test('upgrade arg with out dir', () => { const args = parseArgs(initArgs(['tmp', '--upgrade', 'pathToUpgrade'])); expect(args.upgrade).toBe('pathToUpgrade'); expect(args.out).toBe('tmp'); - expect(args.targetUrl).toBe(''); + expect(args.targetUrl).toBeUndefined(); }); test('upgrade arg with targetUrl', () => { - expect(() => { + expect(() => parseArgs( initArgs(['https://www.google.com', '--upgrade', 'path/to/upgrade']), - ); - }).toThrow(); + ), + ).toThrow(); }); test('multi-inject', () => { @@ -92,53 +92,55 @@ describe('initArgs + parseArgs', () => { }); test.each([ - { arg: 'app-copyright', shortArg: null, value: '(c) Nativefier' }, - { arg: 'app-version', shortArg: null, value: '2.0.0' }, - { arg: 'background-color', shortArg: null, value: '#FFAA88' }, - { arg: 'basic-auth-username', shortArg: null, value: 'user' }, - { arg: 'basic-auth-password', shortArg: null, value: 'p@ssw0rd' }, - { arg: 'bookmarks-menu', shortArg: null, value: 'bookmarks.json' }, + { arg: 'app-copyright', shortArg: '', value: '(c) Nativefier' }, + { arg: 'app-version', shortArg: '', value: '2.0.0' }, + { arg: 'background-color', shortArg: '', value: '#FFAA88' }, + { arg: 'basic-auth-username', shortArg: '', value: 'user' }, + { arg: 'basic-auth-password', shortArg: '', value: 'p@ssw0rd' }, + { arg: 'bookmarks-menu', shortArg: '', value: 'bookmarks.json' }, { arg: 'browserwindow-options', - shortArg: null, + shortArg: '', value: '{"test": 456}', isJsonString: true, }, - { arg: 'build-version', shortArg: null, value: '3.0.0' }, + { arg: 'build-version', shortArg: '', value: '3.0.0' }, { arg: 'crash-reporter', - shortArg: null, + shortArg: '', value: 'https://crash-reporter.com', }, { arg: 'electron-version', shortArg: 'e', value: '1.0.0' }, { arg: 'file-download-options', - shortArg: null, + shortArg: '', value: '{"test": 789}', isJsonString: true, }, - { arg: 'flash-path', shortArg: null, value: 'pathToFlash' }, - { arg: 'global-shortcuts', shortArg: null, value: 'shortcuts.json' }, + { arg: 'flash-path', shortArg: '', value: 'pathToFlash' }, + { arg: 'global-shortcuts', shortArg: '', value: 'shortcuts.json' }, { arg: 'icon', shortArg: 'i', value: 'icon.png' }, - { arg: 'internal-urls', shortArg: null, value: '.*' }, - { arg: 'lang', shortArg: null, value: 'fr' }, + { arg: 'internal-urls', shortArg: '', value: '.*' }, + { arg: 'lang', shortArg: '', value: 'fr' }, { arg: 'name', shortArg: 'n', value: 'Google' }, { arg: 'process-envs', - shortArg: null, + shortArg: '', value: '{"test": 123}', isJsonString: true, }, - { arg: 'proxy-rules', shortArg: null, value: 'RULE: PROXY' }, + { arg: 'proxy-rules', shortArg: '', value: 'RULE: PROXY' }, { arg: 'user-agent', shortArg: 'u', value: 'FIREFOX' }, { arg: 'win32metadata', - shortArg: null, + shortArg: '', value: '{"ProductName": "Google"}', isJsonString: true, }, ])('test string arg %s', ({ arg, shortArg, value, isJsonString }) => { - const args = parseArgs(initArgs(['https://google.com', `--${arg}`, value])); + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`, value]), + ) as Record; if (!isJsonString) { expect(args[arg]).toBe(value); } else { @@ -147,8 +149,8 @@ describe('initArgs + parseArgs', () => { if (shortArg) { const argsShort = parseArgs( - initArgs(['https://google.com', `-${shortArg as string}`, value]), - ); + initArgs(['https://google.com', `-${shortArg}`, value]), + ) as Record; if (!isJsonString) { expect(argsShort[arg]).toBe(value); } else { @@ -162,12 +164,14 @@ describe('initArgs + parseArgs', () => { { arg: 'platform', shortArg: 'p', value: 'mac', badValue: 'os2' }, { arg: 'title-bar-style', - shortArg: null, + shortArg: '', value: 'hidden', badValue: 'cool', }, ])('limited choice arg %s', ({ arg, shortArg, value, badValue }) => { - const args = parseArgs(initArgs(['https://google.com', `--${arg}`, value])); + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`, value]), + ) as Record; expect(args[arg]).toBe(value); // Mock console.error to not pollute the log with the yargs help text @@ -181,7 +185,7 @@ describe('initArgs + parseArgs', () => { if (shortArg) { const argsShort = parseArgs( initArgs(['https://google.com', `-${shortArg}`, value]), - ); + ) as Record; expect(argsShort[arg]).toBe(value); initArgs(['https://google.com', `-${shortArg}`, badValue]); @@ -192,64 +196,74 @@ describe('initArgs + parseArgs', () => { }); test.each([ - { arg: 'always-on-top', shortArg: null }, - { arg: 'block-external-urls', shortArg: null }, - { arg: 'bounce', shortArg: null }, - { arg: 'clear-cache', shortArg: null }, + { arg: 'always-on-top', shortArg: '' }, + { arg: 'block-external-urls', shortArg: '' }, + { arg: 'bounce', shortArg: '' }, + { arg: 'clear-cache', shortArg: '' }, { arg: 'conceal', shortArg: 'c' }, - { arg: 'counter', shortArg: null }, - { arg: 'darwin-dark-mode-support', shortArg: null }, - { arg: 'disable-context-menu', shortArg: null }, - { arg: 'disable-dev-tools', shortArg: null }, - { arg: 'disable-gpu', shortArg: null }, - { arg: 'disable-old-build-warning-yesiknowitisinsecure', shortArg: null }, - { arg: 'enable-es3-apis', shortArg: null }, + { arg: 'counter', shortArg: '' }, + { arg: 'darwin-dark-mode-support', shortArg: '' }, + { arg: 'disable-context-menu', shortArg: '' }, + { arg: 'disable-dev-tools', shortArg: '' }, + { arg: 'disable-gpu', shortArg: '' }, + { arg: 'disable-old-build-warning-yesiknowitisinsecure', shortArg: '' }, + { arg: 'enable-es3-apis', shortArg: '' }, { arg: 'fast-quit', shortArg: 'f' }, - { arg: 'flash', shortArg: null }, - { arg: 'full-screen', shortArg: null }, - { arg: 'hide-window-frame', shortArg: null }, - { arg: 'honest', shortArg: null }, - { arg: 'ignore-certificate', shortArg: null }, - { arg: 'ignore-gpu-blacklist', shortArg: null }, - { arg: 'insecure', shortArg: null }, - { arg: 'maximize', shortArg: null }, - { arg: 'portable', shortArg: null }, + { arg: 'flash', shortArg: '' }, + { arg: 'full-screen', shortArg: '' }, + { arg: 'hide-window-frame', shortArg: '' }, + { arg: 'honest', shortArg: '' }, + { arg: 'ignore-certificate', shortArg: '' }, + { arg: 'ignore-gpu-blacklist', shortArg: '' }, + { arg: 'insecure', shortArg: '' }, + { arg: 'maximize', shortArg: '' }, + { arg: 'portable', shortArg: '' }, { arg: 'show-menu-bar', shortArg: 'm' }, - { arg: 'single-instance', shortArg: null }, - { arg: 'tray', shortArg: null }, - { arg: 'verbose', shortArg: null }, - { arg: 'widevine', shortArg: null }, + { arg: 'single-instance', shortArg: '' }, + { arg: 'tray', shortArg: '' }, + { arg: 'verbose', shortArg: '' }, + { arg: 'widevine', shortArg: '' }, ])('test boolean arg %s', ({ arg, shortArg }) => { - const defaultArgs = parseArgs(initArgs(['https://google.com'])); + const defaultArgs = parseArgs(initArgs(['https://google.com'])) as Record< + string, + boolean + >; expect(defaultArgs[arg]).toBe(false); - const args = parseArgs(initArgs(['https://google.com', `--${arg}`])); + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`]), + ) as Record; expect(args[arg]).toBe(true); if (shortArg) { const argsShort = parseArgs( initArgs(['https://google.com', `-${shortArg}`]), - ); + ) as Record; expect(argsShort[arg]).toBe(true); } }); - test.each([{ arg: 'no-overwrite', shortArg: null }])( + test.each([{ arg: 'no-overwrite', shortArg: '' }])( 'test inversible boolean arg %s', ({ arg, shortArg }) => { const inverse = arg.startsWith('no-') ? arg.substr(3) : `no-${arg}`; - const defaultArgs = parseArgs(initArgs(['https://google.com'])); + const defaultArgs = parseArgs(initArgs(['https://google.com'])) as Record< + string, + boolean + >; expect(defaultArgs[arg]).toBe(false); expect(defaultArgs[inverse]).toBe(true); - const args = parseArgs(initArgs(['https://google.com', `--${arg}`])); + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`]), + ) as Record; expect(args[arg]).toBe(true); expect(args[inverse]).toBe(false); if (shortArg) { const argsShort = parseArgs( - initArgs(['https://google.com', `-${shortArg as string}`]), - ); + initArgs(['https://google.com', `-${shortArg}`]), + ) as Record; expect(argsShort[arg]).toBe(true); expect(argsShort[inverse]).toBe(true); } @@ -257,35 +271,35 @@ describe('initArgs + parseArgs', () => { ); test.each([ - { arg: 'disk-cache-size', shortArg: null, value: 100 }, - { arg: 'height', shortArg: null, value: 200 }, - { arg: 'max-height', shortArg: null, value: 300 }, - { arg: 'max-width', shortArg: null, value: 400 }, - { arg: 'min-height', shortArg: null, value: 500 }, - { arg: 'min-width', shortArg: null, value: 600 }, - { arg: 'width', shortArg: null, value: 700 }, - { arg: 'x', shortArg: null, value: 800 }, - { arg: 'y', shortArg: null, value: 900 }, + { arg: 'disk-cache-size', shortArg: '', value: 100 }, + { arg: 'height', shortArg: '', value: 200 }, + { arg: 'max-height', shortArg: '', value: 300 }, + { arg: 'max-width', shortArg: '', value: 400 }, + { arg: 'min-height', shortArg: '', value: 500 }, + { arg: 'min-width', shortArg: '', value: 600 }, + { arg: 'width', shortArg: '', value: 700 }, + { arg: 'x', shortArg: '', value: 800 }, + { arg: 'y', shortArg: '', value: 900 }, ])('test numeric arg %s', ({ arg, shortArg, value }) => { const args = parseArgs( initArgs(['https://google.com', `--${arg}`, `${value}`]), - ); + ) as Record; expect(args[arg]).toBe(value); const badArgs = parseArgs( initArgs(['https://google.com', `--${arg}`, 'abcd']), - ); + ) as Record; expect(badArgs[arg]).toBeNaN(); if (shortArg) { const shortArgs = parseArgs( - initArgs(['https://google.com', `-${shortArg as string}`, `${value}`]), - ); + initArgs(['https://google.com', `-${shortArg}`, `${value}`]), + ) as Record; expect(shortArgs[arg]).toBe(value); const badShortArgs = parseArgs( - initArgs(['https://google.com', `-${shortArg as string}`, 'abcd']), - ); + initArgs(['https://google.com', `-${shortArg}`, 'abcd']), + ) as Record; expect(badShortArgs[arg]).toBeNaN(); } }); diff --git a/src/cli.ts b/src/cli.ts index 9245bc5..a6ec98d 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,11 +11,12 @@ import { } from './helpers/helpers'; import { supportedArchs, supportedPlatforms } from './infer/inferOs'; import { buildNativefierApp } from './main'; -import { NativefierOptions } from './options/model'; +import { RawOptions } from './options/model'; import { parseJson } from './utils/parseUtils'; import { DEFAULT_ELECTRON_VERSION } from './constants'; +import electronPackager = require('electron-packager'); -export function initArgs(argv: string[]): yargs.Argv { +export function initArgs(argv: string[]): yargs.Argv { const args = yargs(argv) .scriptName('nativefier') .usage( @@ -160,7 +161,6 @@ export function initArgs(argv: string[]): yargs.Argv { coerce: parseJson, description: 'override Electron BrowserWindow options (via JSON string); see https://github.com/nativefier/nativefier/blob/master/API.md#browserwindow-options', - type: 'string', }) .option('disable-context-menu', { default: false, @@ -222,7 +222,6 @@ export function initArgs(argv: string[]): yargs.Argv { coerce: getProcessEnvs, description: 'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened', - type: 'string', }) .option('single-instance', { default: false, @@ -285,11 +284,11 @@ export function initArgs(argv: string[]): yargs.Argv { coerce: parseJson, description: 'a JSON string defining file download options; see https://github.com/sindresorhus/electron-dl', - type: 'string', }) .option('inject', { description: 'path to a CSS/JS file to be injected; pass multiple times to inject multiple files', + string: true, type: 'array', }) .option('lang', { @@ -477,10 +476,10 @@ export function initArgs(argv: string[]): yargs.Argv { type: 'string', }) .option('win32metadata', { - coerce: parseJson, + coerce: (value: string) => + parseJson(value), description: '(windows only) a JSON string of key/value pairs (ProductName, InternalName, FileDescription) to embed as executable metadata', - type: 'string', }) .group( [ @@ -526,17 +525,17 @@ function decorateYargOptionGroup(value: string): string { return `====== ${value} ======`; } -export function parseArgs(args: yargs.Argv): any { +export function parseArgs(args: yargs.Argv): RawOptions { const parsed = { ...args.argv }; // In yargs, the _ property of the parsed args is an array of the positional args // https://github.com/yargs/yargs/blob/master/docs/examples.md#and-non-hyphenated-options-too-just-use-argv_ // So try to extract the targetUrl and outputDirectory from these - parsed.targetUrl = parsed._.length > 0 ? parsed._[0].toString() : ''; - parsed.out = parsed._.length > 1 ? parsed._[1] : ''; + parsed.targetUrl = parsed._.length > 0 ? parsed._[0].toString() : undefined; + parsed.out = parsed._.length > 1 ? (parsed._[1] as string) : undefined; - if (parsed.upgrade && parsed.targetUrl !== '') { + if (parsed.upgrade && parsed.targetUrl) { let targetAndUpgrade = false; - if (parsed.out === '') { + if (!parsed.out) { // If we're upgrading, the first positional args might be the outputDirectory, so swap these if we can try { // If this succeeds, we have a problem @@ -545,7 +544,7 @@ export function parseArgs(args: yargs.Argv): any { } catch { // Cool, it's not a URL parsed.out = parsed.targetUrl; - parsed.targetUrl = ''; + parsed.targetUrl = undefined; } } else { // Someone supplied a targetUrl, an outputDirectory, and --upgrade. That's not cool. @@ -559,7 +558,7 @@ export function parseArgs(args: yargs.Argv): any { } } - if (parsed.targetUrl === '' && !parsed.upgrade) { + if (!parsed.targetUrl && !parsed.upgrade) { throw new Error( 'ERROR: Nativefier must be called with either a targetUrl or the --upgrade option.\n', ); @@ -572,7 +571,7 @@ export function parseArgs(args: yargs.Argv): any { if (require.main === module) { // Not sure if we still need this with yargs. Keeping for now. - const sanitizedArgs = []; + const sanitizedArgs: string[] = []; process.argv.forEach((arg) => { if (isArgFormatInvalid(arg)) { throw new Error( @@ -593,11 +592,12 @@ if (require.main === module) { sanitizedArgs.push(arg); }); - let args, parsedArgs; + let args: yargs.Argv | undefined = undefined; + let parsedArgs: RawOptions; try { args = initArgs(sanitizedArgs.slice(2)); parsedArgs = parseArgs(args); - } catch (err) { + } catch (err: unknown) { if (args) { log.error(err); args.showHelp(); @@ -607,7 +607,7 @@ if (require.main === module) { process.exit(1); } - const options: NativefierOptions = { + const options: RawOptions = { ...parsedArgs, }; diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts index ae02556..befbb79 100644 --- a/src/helpers/helpers.ts +++ b/src/helpers/helpers.ts @@ -17,11 +17,17 @@ tmp.setGracefulCleanup(); // cleanup temp dirs even when an uncaught exception o const now = new Date(); const TMP_TIME = `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`; -type DownloadResult = { +export type DownloadResult = { data: Buffer; ext: string; }; +type ProcessEnvs = Record; + +export function hasWine(): boolean { + return hasbin.sync('wine'); +} + export function isOSX(): boolean { return os.platform() === 'darwin'; } @@ -57,7 +63,7 @@ export async function copyFileOrDir( dest: string, ): Promise { return new Promise((resolve, reject) => { - ncp(sourceFileOrDir, dest, (error: any) => { + ncp(sourceFileOrDir, dest, (error: Error[] | null): void => { if (error) { reject(error); } @@ -66,15 +72,17 @@ export async function copyFileOrDir( }); } -export function downloadFile(fileUrl: string): Promise { +export function downloadFile( + fileUrl: string, +): Promise { log.debug(`Downloading ${fileUrl}`); return axios - .get(fileUrl, { + .get(fileUrl, { responseType: 'arraybuffer', }) .then((response) => { if (!response.data) { - return null; + return undefined; } return { data: response.data, @@ -97,7 +105,7 @@ export function getAllowedIconFormats(platform: string): string[] { const icnsToPng = false; const icnsToIco = false; - const formats = []; + const formats: string[] = []; // Shell scripting is not supported on windows, temporary override if (isWindows()) { @@ -175,11 +183,11 @@ export function generateRandomSuffix(length = 6): string { return hash.digest('hex').substring(0, length); } -export function getProcessEnvs(val: string): any { +export function getProcessEnvs(val: string): ProcessEnvs | undefined { if (!val) { - return {}; + return undefined; } - return parseJson(val); + return parseJson(val); } export function checkInternet(): void { diff --git a/src/helpers/upgrade/executableHelpers.ts b/src/helpers/upgrade/executableHelpers.ts index eaabb5f..2ec3348 100644 --- a/src/helpers/upgrade/executableHelpers.ts +++ b/src/helpers/upgrade/executableHelpers.ts @@ -65,9 +65,9 @@ function getExecutableArch( function getExecutableInfo( executablePath: string, platform: string, -): ExecutableInfo { +): ExecutableInfo | undefined { if (!fileExists(executablePath)) { - return {}; + return undefined; } const exeBytes = getExecutableBytes(executablePath); @@ -81,6 +81,12 @@ export function getOptionsFromExecutable( priorOptions: NativefierOptions, ): NativefierOptions { const newOptions: NativefierOptions = { ...priorOptions }; + if (!newOptions.name) { + throw new Error( + 'Can not extract options from executable with no name specified.', + ); + } + const name: string = newOptions.name; let executablePath: string | undefined = undefined; const appRoot = path.resolve(path.join(appResourcesDir, '..', '..')); @@ -118,8 +124,7 @@ export function getOptionsFromExecutable( appRoot, children.filter( (c) => - c.name.toLowerCase() === `${newOptions.name.toLowerCase()}.exe` && - c.isFile(), + c.name.toLowerCase() === `${name.toLowerCase()}.exe` && c.isFile(), )[0].name, ); @@ -130,7 +135,9 @@ export function getOptionsFromExecutable( 'ProductVersion', ); log.debug( - `Extracted app version from executable: ${newOptions.appVersion}`, + `Extracted app version from executable: ${ + newOptions.appVersion as string + }`, ); } @@ -142,7 +149,9 @@ export function getOptionsFromExecutable( newOptions.buildVersion = undefined; } else { log.debug( - `Extracted build version from executable: ${newOptions.buildVersion}`, + `Extracted build version from executable: ${ + newOptions.buildVersion as string + }`, ); } } @@ -154,7 +163,9 @@ export function getOptionsFromExecutable( 'LegalCopyright', ); log.debug( - `Extracted app copyright from executable: ${newOptions.appCopyright}`, + `Extracted app copyright from executable: ${ + newOptions.appCopyright as string + }`, ); } } else if (looksLikeLinux) { @@ -164,7 +175,13 @@ export function getOptionsFromExecutable( } executablePath = path.join( appRoot, - children.filter((c) => c.name == newOptions.name && c.isFile())[0].name, + children.filter((c) => c.name == name && c.isFile())[0].name, + ); + } + + if (!executablePath || !newOptions.platform) { + throw Error( + `Could not find executablePath or platform of app in ${appRoot}`, ); } @@ -175,8 +192,14 @@ export function getOptionsFromExecutable( executablePath, newOptions.platform, ); + if (!executableInfo) { + throw new Error( + `Could not get executable info for executable path: ${executablePath}`, + ); + } + newOptions.arch = executableInfo.arch; - log.debug(`Extracted arch from executable: ${newOptions.arch}`); + log.debug(`Extracted arch from executable: ${newOptions.arch as string}`); } if (newOptions.platform === undefined || newOptions.arch == undefined) { throw Error(`Could not determine platform / arch of app in ${appRoot}`); diff --git a/src/helpers/upgrade/rceditGet.ts b/src/helpers/upgrade/rceditGet.ts index 3d2b79f..4ba75e1 100644 --- a/src/helpers/upgrade/rceditGet.ts +++ b/src/helpers/upgrade/rceditGet.ts @@ -8,7 +8,7 @@ import { spawnSync } from 'child_process'; export function getVersionString( executablePath: string, versionString: string, -): string { +): string | undefined { let rcedit = path.resolve( __dirname, '..', diff --git a/src/helpers/upgrade/upgrade.ts b/src/helpers/upgrade/upgrade.ts index 4802d50..8a9f7c3 100644 --- a/src/helpers/upgrade/upgrade.ts +++ b/src/helpers/upgrade/upgrade.ts @@ -3,10 +3,11 @@ import * as path from 'path'; import * as log from 'loglevel'; -import { NativefierOptions } from '../../options/model'; +import { NativefierOptions, RawOptions } from '../../options/model'; import { dirExists, fileExists } from '../fsHelpers'; import { extractBoolean, extractString } from './plistInfoXMLHelpers'; import { getOptionsFromExecutable } from './executableHelpers'; +import { parseJson } from '../../utils/parseUtils'; export type UpgradeAppInfo = { appResourcesDir: string; @@ -79,7 +80,9 @@ function getInfoPListOptions( 'NSHumanReadableCopyright', ); log.debug( - `Extracted app copyright from Info.plist: ${newOptions.appCopyright}`, + `Extracted app copyright from Info.plist: ${ + newOptions.appCopyright as string + }`, ); } @@ -96,7 +99,9 @@ function getInfoPListOptions( ? undefined : newOptions.darwinDarkModeSupport === false), log.debug( - `Extracted app version from Info.plist: ${newOptions.appVersion}`, + `Extracted app version from Info.plist: ${ + newOptions.appVersion as string + }`, ); } @@ -152,11 +157,19 @@ export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null { return null; } - log.debug(`Loading ${path.join(appResourcesDir, 'nativefier.json')}`); - const options: NativefierOptions = JSON.parse( - fs.readFileSync(path.join(appResourcesDir, 'nativefier.json'), 'utf8'), + const nativefierJSONPath = path.join(appResourcesDir, 'nativefier.json'); + + log.debug(`Loading ${nativefierJSONPath}`); + const options = parseJson( + fs.readFileSync(nativefierJSONPath, 'utf8'), ); + if (!options) { + throw new Error( + `Could not read Nativefier options from ${nativefierJSONPath}`, + ); + } + options.electronVersion = undefined; return { @@ -173,9 +186,9 @@ export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null { } export function useOldAppOptions( - rawOptions: NativefierOptions, + rawOptions: RawOptions, oldApp: UpgradeAppInfo, -): NativefierOptions { +): RawOptions { if (rawOptions.targetUrl !== undefined && dirExists(rawOptions.targetUrl)) { // You got your ouput dir in my targetUrl! rawOptions.out = rawOptions.targetUrl; diff --git a/src/infer/browsers/inferChromeVersion.ts b/src/infer/browsers/inferChromeVersion.ts index 7638262..074e30e 100644 --- a/src/infer/browsers/inferChromeVersion.ts +++ b/src/infer/browsers/inferChromeVersion.ts @@ -31,7 +31,7 @@ export async function getChromeVersionForElectronVersion( try { log.debug('Grabbing electron<->chrome versions file from', url); - const response = await axios.get(url, { timeout: 5000 }); + const response = await axios.get(url, { timeout: 5000 }); if (response.status !== 200) { throw new Error(`Bad request: Status code ${response.status}`); } @@ -50,7 +50,7 @@ export async function getChromeVersionForElectronVersion( `Associated electron v${electronVersion} to chrome v${chromeVersion}`, ); return chromeVersion; - } catch (err) { + } catch (err: unknown) { log.error('getChromeVersionForElectronVersion ERROR', err); log.debug('Falling back to default Chrome version', DEFAULT_CHROME_VERSION); return DEFAULT_CHROME_VERSION; diff --git a/src/infer/browsers/inferFirefoxVersion.ts b/src/infer/browsers/inferFirefoxVersion.ts index ea675a7..478a299 100644 --- a/src/infer/browsers/inferFirefoxVersion.ts +++ b/src/infer/browsers/inferFirefoxVersion.ts @@ -28,7 +28,7 @@ export async function getLatestFirefoxVersion( ): Promise { try { log.debug('Grabbing Firefox version data from', url); - const response = await axios.get(url, { timeout: 5000 }); + const response = await axios.get(url, { timeout: 5000 }); if (response.status !== 200) { throw new Error(`Bad request: Status code ${response.status}`); } @@ -38,7 +38,7 @@ export async function getLatestFirefoxVersion( `Got latest Firefox version ${firefoxVersions.LATEST_FIREFOX_VERSION}`, ); return firefoxVersions.LATEST_FIREFOX_VERSION; - } catch (err) { + } catch (err: unknown) { log.error('getLatestFirefoxVersion ERROR', err); log.debug( 'Falling back to default Firefox version', diff --git a/src/infer/browsers/inferSafariVersion.ts b/src/infer/browsers/inferSafariVersion.ts index 00ab0da..c35af6c 100644 --- a/src/infer/browsers/inferSafariVersion.ts +++ b/src/infer/browsers/inferSafariVersion.ts @@ -16,7 +16,7 @@ export async function getLatestSafariVersion( ): Promise { try { log.debug('Grabbing apple version data from', url); - const response = await axios.get(url, { timeout: 5000 }); + const response = await axios.get(url, { timeout: 5000 }); if (response.status !== 200) { throw new Error(`Bad request: Status code ${response.status}`); } @@ -39,8 +39,8 @@ export async function getLatestSafariVersion( const versionRows = majorVersionTable.split(' { const currentScore = currentIcon.score; - if (currentScore > maxScore) { + if (currentScore && currentScore > maxScore) { return currentScore; } return maxScore; @@ -29,36 +37,42 @@ function getMaxMatchScore(iconWithScores: any[]): number { return score; } -function getMatchingIcons(iconsWithScores: any[], maxScore: number): any[] { - return iconsWithScores - .filter((item) => item.score === maxScore) - .map((item) => ({ ...item, ext: path.extname(item.url) })); +function getMatchingIcons( + iconsWithScores: GitCloudIcon[], + maxScore: number, +): GitCloudIcon[] { + return iconsWithScores.filter((item) => item.score === maxScore); } function mapIconWithMatchScore( cloudIcons: { name: string; url: string }[], targetUrl: string, -): any { +): GitCloudIcon[] { const normalisedTargetUrl = targetUrl.toLowerCase(); return cloudIcons.map((item) => { const itemWords = item.name.split(GITCLOUD_SPACE_DELIMITER); - const score = itemWords.reduce((currentScore: number, word: string) => { - if (normalisedTargetUrl.includes(word)) { - return currentScore + 1; - } - return currentScore; - }, 0); + const score: number = itemWords.reduce( + (currentScore: number, word: string) => { + if (normalisedTargetUrl.includes(word)) { + return currentScore + 1; + } + return currentScore; + }, + 0, + ); - return { ...item, score }; + return { ...item, ext: path.extname(item.url), score }; }); } async function inferIconFromStore( targetUrl: string, platform: string, -): Promise { +): Promise { log.debug(`Inferring icon from store for ${targetUrl} on ${platform}`); - const allowedFormats = new Set(getAllowedIconFormats(platform)); + const allowedFormats = new Set( + getAllowedIconFormats(platform), + ); const cloudIcons = await gitCloud(GITCLOUD_URL); log.debug(`Got ${cloudIcons.length} icons from gitcloud`); @@ -67,19 +81,19 @@ async function inferIconFromStore( if (maxScore === 0) { log.debug('No relevant icon in store.'); - return null; + return undefined; } const iconsMatchingScore = getMatchingIcons(iconWithScores, maxScore); const iconsMatchingExt = iconsMatchingScore.filter((icon) => - allowedFormats.has(icon.ext), + allowedFormats.has(icon.ext ?? path.extname(icon.url as string)), ); const matchingIcon = iconsMatchingExt[0]; const iconUrl = matchingIcon && matchingIcon.url; if (!iconUrl) { log.debug('Could not infer icon from store'); - return null; + return undefined; } return downloadFile(iconUrl); } @@ -87,21 +101,19 @@ async function inferIconFromStore( export async function inferIcon( targetUrl: string, platform: string, -): Promise { +): Promise { log.debug(`Inferring icon for ${targetUrl} on ${platform}`); const tmpDirPath = getTempDir('iconinfer'); - let icon: { ext: string; data: Buffer } = await inferIconFromStore( - targetUrl, - platform, - ); + let icon: { ext: string; data: Buffer } | undefined = + await inferIconFromStore(targetUrl, platform); if (!icon) { const ext = platform === 'win32' ? '.ico' : '.png'; log.debug(`Trying to extract a ${ext} icon from the page.`); icon = await pageIcon(targetUrl, { ext }); } if (!icon) { - return null; + return undefined; } log.debug(`Got an icon from the page.`); diff --git a/src/infer/inferOs.ts b/src/infer/inferOs.ts index 7a8500c..1f58901 100644 --- a/src/infer/inferOs.ts +++ b/src/infer/inferOs.ts @@ -19,13 +19,7 @@ export const supportedPlatforms = [ export function inferPlatform(): string { const platform = os.platform(); - if ( - platform === 'darwin' || - // @ts-ignore - platform === 'mas' || - platform === 'win32' || - platform === 'linux' - ) { + if (['darwin', 'linux', 'win32'].includes(platform)) { log.debug('Inferred platform', platform); return platform; } diff --git a/src/infer/inferTitle.ts b/src/infer/inferTitle.ts index 04728c4..744bccf 100644 --- a/src/infer/inferTitle.ts +++ b/src/infer/inferTitle.ts @@ -5,7 +5,7 @@ const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36'; export async function inferTitle(url: string): Promise { - const { data } = await axios.get(url, { + const { data } = await axios.get(url, { headers: { // Fake user agent for pages like http://messenger.com 'User-Agent': USER_AGENT, @@ -13,7 +13,7 @@ export async function inferTitle(url: string): Promise { }); log.debug(`Fetched ${(data.length / 1024).toFixed(1)} kb page at`, url); const inferredTitle = - /<\s*title.*?>(?.+?)<\s*\/title\s*?>/i.exec(data).groups?.title || + /<\s*title.*?>(?<title>.+?)<\s*\/title\s*?>/i.exec(data)?.groups?.title ?? 'Webapp'; log.debug('Inferred title:', inferredTitle); return inferredTitle; diff --git a/src/integration-test.ts b/src/integration-test.ts index 13db378..61d965f 100644 --- a/src/integration-test.ts +++ b/src/integration-test.ts @@ -10,9 +10,14 @@ import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion'; import { inferArch } from './infer/inferOs'; import { buildNativefierApp } from './main'; import { userAgent } from './options/fields/userAgent'; +import { NativefierOptions } from './options/model'; +import { parseJson } from './utils/parseUtils'; -async function checkApp(appRoot: string, inputOptions: any): Promise<void> { - const arch = (inputOptions.arch as string) || inferArch(); +async function checkApp( + appRoot: string, + inputOptions: NativefierOptions, +): Promise<void> { + const arch = inputOptions.arch ? (inputOptions.arch as string) : inferArch(); if (inputOptions.out !== undefined) { expect( path.join( @@ -43,28 +48,31 @@ async function checkApp(appRoot: string, inputOptions: any): Promise<void> { const appPath = path.join(appRoot, relativeAppFolder); const configPath = path.join(appPath, 'nativefier.json'); - const nativefierConfig = JSON.parse(fs.readFileSync(configPath).toString()); - expect(inputOptions.targetUrl).toBe(nativefierConfig.targetUrl); + const nativefierConfig: NativefierOptions | undefined = + parseJson<NativefierOptions>(fs.readFileSync(configPath).toString()); + expect(nativefierConfig).not.toBeUndefined(); + + expect(inputOptions.targetUrl).toBe(nativefierConfig?.targetUrl); // Test name inferring - expect(nativefierConfig.name).toBe('Google'); + expect(nativefierConfig?.name).toBe('Google'); // Test icon writing const iconFile = inputOptions.platform === 'darwin' ? '../electron.icns' : 'icon.png'; const iconPath = path.join(appPath, iconFile); - expect(fs.existsSync(iconPath)).toBe(true); + expect(fs.existsSync(iconPath)).toEqual(true); expect(fs.statSync(iconPath).size).toBeGreaterThan(1000); // Test arch if (inputOptions.arch !== undefined) { - expect(inputOptions.arch).toBe(nativefierConfig.arch); + expect(inputOptions.arch).toEqual(nativefierConfig?.arch); } else { - expect(os.arch()).toBe(nativefierConfig.arch); + expect(os.arch()).toEqual(nativefierConfig?.arch); } // Test electron version - expect(nativefierConfig.electronVersionUsed).toBe( + expect(nativefierConfig?.electronVersionUsed).toBe( inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION, ); @@ -80,10 +88,11 @@ async function checkApp(appRoot: string, inputOptions: any): Promise<void> { }); inputOptions.userAgent = translatedUserAgent || inputOptions.userAgent; } - expect(nativefierConfig.userAgent).toBe(inputOptions.userAgent); + + expect(nativefierConfig?.userAgent).toEqual(inputOptions.userAgent); // Test lang - expect(nativefierConfig.lang).toBe(inputOptions.lang); + expect(nativefierConfig?.lang).toEqual(inputOptions.lang); } describe('Nativefier', () => { @@ -101,7 +110,8 @@ describe('Nativefier', () => { lang: 'en-US', }; const appPath = await buildNativefierApp(options); - await checkApp(appPath, options); + expect(appPath).not.toBeUndefined(); + await checkApp(appPath as string, options); }, ); }); @@ -132,7 +142,8 @@ describe('Nativefier upgrade', () => { ...baseAppOptions, }; const appPath = await buildNativefierApp(options); - await checkApp(appPath, options); + expect(appPath).not.toBeUndefined(); + await checkApp(appPath as string, options); const upgradeOptions = { upgrade: appPath, @@ -142,7 +153,8 @@ describe('Nativefier upgrade', () => { const upgradeAppPath = await buildNativefierApp(upgradeOptions); options.electronVersion = DEFAULT_ELECTRON_VERSION; options.userAgent = baseAppOptions.userAgent; - await checkApp(upgradeAppPath, options); + expect(upgradeAppPath).not.toBeUndefined(); + await checkApp(upgradeAppPath as string, options); }, ); }); diff --git a/src/main.ts b/src/main.ts index c453d53..98be281 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ import 'source-map-support/register'; import { buildNativefierApp } from './build/buildNativefierApp'; +import { RawOptions } from './options/model'; export { buildNativefierApp }; @@ -9,12 +10,12 @@ export { buildNativefierApp }; * Use the better, modern async `buildNativefierApp` instead if you can! */ function buildNativefierAppOldCallbackStyle( - options: any, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types - callback: (err: any, result?: any) => void, + options: RawOptions, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types + callback: (err?: Error, result?: string) => void, ): void { buildNativefierApp(options) - .then((result) => callback(null, result)) - .catch((err) => callback(err)); + .then((result) => callback(undefined, result)) + .catch((err: Error) => callback(err)); } export default buildNativefierAppOldCallbackStyle; diff --git a/src/options/asyncConfig.ts b/src/options/asyncConfig.ts index 7e4edec..3f72618 100644 --- a/src/options/asyncConfig.ts +++ b/src/options/asyncConfig.ts @@ -6,7 +6,7 @@ import { AppOptions } from './model'; /** * Takes the options object and infers new values needing async work */ -export async function asyncConfig(options: AppOptions): Promise<any> { +export async function asyncConfig(options: AppOptions): Promise<AppOptions> { log.debug('\nPerforming async options post-processing.'); - await processOptions(options); + return await processOptions(options); } diff --git a/src/options/fields/fields.test.ts b/src/options/fields/fields.test.ts index 1390bf1..b1e9a10 100644 --- a/src/options/fields/fields.test.ts +++ b/src/options/fields/fields.test.ts @@ -1,53 +1,103 @@ +import { AppOptions } from '../model'; import { processOptions } from './fields'; +describe('fields', () => { + let options: AppOptions; -test('fully-defined async options are returned as-is', async () => { - const options = { - packager: { - icon: '/my/icon.png', - name: 'my beautiful app ', - targetUrl: 'https://myurl.com', - dir: '/tmp/myapp', - }, - nativefier: { userAgent: 'random user agent' }, - }; - // @ts-ignore - await processOptions(options); + beforeEach(() => { + options = { + nativefier: { + accessibilityPrompt: false, + alwaysOnTop: false, + backgroundColor: undefined, + basicAuthPassword: undefined, + basicAuthUsername: undefined, + blockExternalUrls: false, + bookmarksMenu: undefined, + bounce: false, + browserwindowOptions: undefined, + clearCache: false, + counter: false, + crashReporter: undefined, + disableContextMenu: false, + disableDevTools: false, + disableGpu: false, + disableOldBuildWarning: false, + diskCacheSize: undefined, + enableEs3Apis: false, + fastQuit: true, + fileDownloadOptions: undefined, + flashPluginDir: undefined, + fullScreen: false, + globalShortcuts: undefined, + height: undefined, + hideWindowFrame: false, + ignoreCertificate: false, + ignoreGpuBlacklist: false, + inject: [], + insecure: false, + internalUrls: undefined, + maximize: false, + maxHeight: undefined, + minWidth: undefined, + minHeight: undefined, + maxWidth: undefined, + nativefierVersion: '1.0.0', + processEnvs: undefined, + proxyRules: undefined, + showMenuBar: false, + singleInstance: false, + titleBarStyle: undefined, + tray: false, + userAgent: undefined, + userAgentHonest: false, + verbose: false, + versionString: '1.0.0', + width: undefined, + widevine: false, + x: undefined, + y: undefined, + zoom: 1, + }, + packager: { + dir: '', + platform: process.platform, + portable: false, + targetUrl: '', + upgrade: false, + }, + }; + }); - expect(options.packager.icon).toEqual('/my/icon.png'); - expect(options.packager.name).toEqual('my beautiful app'); - expect(options.nativefier.userAgent).toEqual('random user agent'); -}); - -test('user agent is ignored if not provided', async () => { - const options = { - packager: { - icon: '/my/icon.png', - name: 'my beautiful app ', - targetUrl: 'https://myurl.com', - dir: '/tmp/myapp', - platform: 'linux', - }, - nativefier: { userAgent: undefined }, - }; - // @ts-ignore - await processOptions(options); - - expect(options.nativefier.userAgent).toBeUndefined(); -}); - -test('user agent short code is populated', async () => { - const options = { - packager: { - icon: '/my/icon.png', - name: 'my beautiful app ', - targetUrl: 'https://myurl.com', - dir: '/tmp/myapp', - platform: 'linux', - }, - nativefier: { userAgent: 'edge' }, - }; - // @ts-ignore - await processOptions(options); - - expect(options.nativefier.userAgent).not.toBe('edge'); + test('fully-defined async options are returned as-is', async () => { + options.packager.icon = '/my/icon.png'; + options.packager.name = 'my beautiful app '; + options.packager.platform = 'darwin'; + options.nativefier.userAgent = 'random user agent'; + await processOptions(options); + + expect(options.packager.icon).toEqual('/my/icon.png'); + expect(options.packager.name).toEqual('my beautiful app'); + expect(options.nativefier.userAgent).toEqual('random user agent'); + }); + + test('name has spaces stripped in linux', async () => { + options.packager.name = 'my beautiful app '; + options.packager.platform = 'linux'; + await processOptions(options); + + expect(options.packager.name).toEqual('mybeautifulapp'); + }); + + test('user agent is ignored if not provided', async () => { + await processOptions(options); + + expect(options.nativefier.userAgent).toBeUndefined(); + }); + + test('user agent short code is populated', async () => { + options.nativefier.userAgent = 'edge'; + await processOptions(options); + + expect(options.nativefier.userAgent).not.toBe('edge'); + }); }); diff --git a/src/options/fields/fields.ts b/src/options/fields/fields.ts index 0ff7718..c516073 100644 --- a/src/options/fields/fields.ts +++ b/src/options/fields/fields.ts @@ -3,13 +3,19 @@ import { userAgent } from './userAgent'; import { AppOptions } from '../model'; import { name } from './name'; -const OPTION_POSTPROCESSORS = [ +type OptionPostprocessor = { + namespace: 'nativefier' | 'packager'; + option: 'icon' | 'name' | 'userAgent'; + processor: (options: AppOptions) => Promise<string | undefined>; +}; + +const OPTION_POSTPROCESSORS: OptionPostprocessor[] = [ { namespace: 'nativefier', option: 'userAgent', processor: userAgent }, { namespace: 'packager', option: 'icon', processor: icon }, { namespace: 'packager', option: 'name', processor: name }, ]; -export async function processOptions(options: AppOptions): Promise<void> { +export async function processOptions(options: AppOptions): Promise<AppOptions> { const processedOptions = await Promise.all( OPTION_POSTPROCESSORS.map(async ({ namespace, option, processor }) => { const result = await processor(options); @@ -22,8 +28,15 @@ export async function processOptions(options: AppOptions): Promise<void> { ); for (const { namespace, option, result } of processedOptions) { - if (result !== null) { + if ( + result && + namespace in options && + options[namespace] && + option in options[namespace] + ) { + // @ts-expect-error We're fiddling with objects at the string key level, which TS doesn't support well. options[namespace][option] = result; } } + return options; } diff --git a/src/options/fields/icon.test.ts b/src/options/fields/icon.test.ts index ca1f300..64dfdbd 100644 --- a/src/options/fields/icon.test.ts +++ b/src/options/fields/icon.test.ts @@ -24,7 +24,7 @@ const ICON_PARAMS_NEEDS_INFER = { describe('when the icon parameter is passed', () => { test('it should return the icon parameter', async () => { expect(inferIcon).toHaveBeenCalledTimes(0); - await expect(icon(ICON_PARAMS_PROVIDED)).resolves.toBe(null); + await expect(icon(ICON_PARAMS_PROVIDED)).resolves.toBeUndefined(); }); }); @@ -49,7 +49,7 @@ describe('when the icon parameter is not passed', () => { ); const result = await icon(ICON_PARAMS_NEEDS_INFER); - expect(result).toBe(null); + expect(result).toBeUndefined(); expect(inferIcon).toHaveBeenCalledWith( ICON_PARAMS_NEEDS_INFER.packager.targetUrl, ICON_PARAMS_NEEDS_INFER.packager.platform, diff --git a/src/options/fields/icon.ts b/src/options/fields/icon.ts index 7133e69..0612d78 100644 --- a/src/options/fields/icon.ts +++ b/src/options/fields/icon.ts @@ -10,10 +10,15 @@ type IconParams = { }; }; -export async function icon(options: IconParams): Promise<string> { +export async function icon(options: IconParams): Promise<string | undefined> { if (options.packager.icon) { log.debug('Got icon from options. Using it, no inferring needed'); - return null; + return undefined; + } + + if (!options.packager.platform) { + log.error('No platform specified. Icon can not be inferrerd.'); + return undefined; } try { @@ -21,11 +26,8 @@ export async function icon(options: IconParams): Promise<string> { options.packager.targetUrl, options.packager.platform, ); - } catch (error) { - log.warn( - 'Cannot automatically retrieve the app icon:', - error.message || '', - ); - return null; + } catch (err: unknown) { + log.warn('Cannot automatically retrieve the app icon:', err); + return undefined; } } diff --git a/src/options/fields/name.ts b/src/options/fields/name.ts index d2174ef..4573658 100644 --- a/src/options/fields/name.ts +++ b/src/options/fields/name.ts @@ -17,24 +17,20 @@ async function tryToInferName(targetUrl: string): Promise<string> { log.debug('Inferring name for', targetUrl); const pageTitle = await inferTitle(targetUrl); return pageTitle || DEFAULT_APP_NAME; - } catch (err) { + } catch (err: unknown) { log.warn( - `Unable to automatically determine app name, falling back to '${DEFAULT_APP_NAME}'. Reason: ${( - err as Error - ).toString()}`, + `Unable to automatically determine app name, falling back to '${DEFAULT_APP_NAME}'.`, + err, ); return DEFAULT_APP_NAME; } } export async function name(options: NameParams): Promise<string> { - if (options.packager.name) { - log.debug( - `Got name ${options.packager.name} from options. No inferring needed`, - ); - return sanitizeFilename(options.packager.platform, options.packager.name); + let name: string | undefined = options.packager.name; + if (!name) { + name = await tryToInferName(options.packager.targetUrl); } - const inferredName = await tryToInferName(options.packager.targetUrl); - return sanitizeFilename(options.packager.platform, inferredName); + return sanitizeFilename(options.packager.platform, name); } diff --git a/src/options/fields/userAgent.test.ts b/src/options/fields/userAgent.test.ts index e1b5028..97bcd44 100644 --- a/src/options/fields/userAgent.test.ts +++ b/src/options/fields/userAgent.test.ts @@ -12,7 +12,7 @@ test('when a userAgent parameter is passed', async () => { packager: {}, nativefier: { userAgent: 'valid user agent' }, }; - await expect(userAgent(params)).resolves.toBeNull(); + await expect(userAgent(params)).resolves.toBeUndefined(); }); test('no userAgent parameter is passed', async () => { @@ -20,7 +20,7 @@ test('no userAgent parameter is passed', async () => { packager: { platform: 'mac' }, nativefier: {}, }; - await expect(userAgent(params)).resolves.toBeNull(); + await expect(userAgent(params)).resolves.toBeUndefined(); }); test('edge userAgent parameter is passed', async () => { diff --git a/src/options/fields/userAgent.ts b/src/options/fields/userAgent.ts index cb7e8ba..05007fd 100644 --- a/src/options/fields/userAgent.ts +++ b/src/options/fields/userAgent.ts @@ -1,11 +1,12 @@ import * as log from 'loglevel'; +import { DEFAULT_ELECTRON_VERSION } from '../../constants'; import { getChromeVersionForElectronVersion } from '../../infer/browsers/inferChromeVersion'; import { getLatestFirefoxVersion } from '../../infer/browsers/inferFirefoxVersion'; import { getLatestSafariVersion } from '../../infer/browsers/inferSafariVersion'; import { normalizePlatform } from '../optionsMain'; -type UserAgentOpts = { +export type UserAgentOpts = { packager: { electronVersion?: string; platform?: string; @@ -15,22 +16,27 @@ type UserAgentOpts = { }; }; -const USER_AGENT_PLATFORM_MAPS = { +const USER_AGENT_PLATFORM_MAPS: Record<string, string> = { darwin: 'Macintosh; Intel Mac OS X 10_15_7', linux: 'X11; Linux x86_64', win32: 'Windows NT 10.0; Win64; x64', }; -const USER_AGENT_SHORT_CODE_MAPS = { +const USER_AGENT_SHORT_CODE_MAPS: Record< + string, + (platform: string, electronVersion: string) => Promise<string> +> = { edge: edgeUserAgent, firefox: firefoxUserAgent, safari: safariUserAgent, }; -export async function userAgent(options: UserAgentOpts): Promise<string> { +export async function userAgent( + options: UserAgentOpts, +): Promise<string | undefined> { if (!options.nativefier.userAgent) { // No user agent got passed. Let's handle it with the app.userAgentFallback - return null; + return undefined; } if ( @@ -43,19 +49,22 @@ export async function userAgent(options: UserAgentOpts): Promise<string> { `${options.nativefier.userAgent.toLowerCase()} not found in`, Object.keys(USER_AGENT_SHORT_CODE_MAPS), ); - return null; + return undefined; } options.packager.platform = normalizePlatform(options.packager.platform); - const userAgentPlatform = + const userAgentPlatform: string = USER_AGENT_PLATFORM_MAPS[ options.packager.platform === 'mas' ? 'darwin' : options.packager.platform ]; const mapFunction = USER_AGENT_SHORT_CODE_MAPS[options.nativefier.userAgent]; - return await mapFunction(userAgentPlatform, options.packager.electronVersion); + return await mapFunction( + userAgentPlatform, + options.packager.electronVersion ?? DEFAULT_ELECTRON_VERSION, + ); } async function edgeUserAgent( diff --git a/src/options/model.ts b/src/options/model.ts index da5993d..a6d4f44 100644 --- a/src/options/model.ts +++ b/src/options/model.ts @@ -1,8 +1,9 @@ +import { CreateOptions } from 'asar'; import * as electronPackager from 'electron-packager'; export interface ElectronPackagerOptions extends electronPackager.Options { portable: boolean; - platform: string; + platform?: string; targetUrl: string; upgrade: boolean; upgradeFrom?: string; @@ -13,60 +14,152 @@ export interface AppOptions { nativefier: { accessibilityPrompt: boolean; alwaysOnTop: boolean; - backgroundColor: string; - basicAuthPassword: string; - basicAuthUsername: string; + backgroundColor?: string; + basicAuthPassword?: string; + basicAuthUsername?: string; blockExternalUrls: boolean; - bookmarksMenu: string; + bookmarksMenu?: string; bounce: boolean; - browserwindowOptions: any; + browserwindowOptions?: BrowserWindowOptions; clearCache: boolean; counter: boolean; - crashReporter: string; + crashReporter?: string; disableContextMenu: boolean; disableDevTools: boolean; disableGpu: boolean; disableOldBuildWarning: boolean; - diskCacheSize: number; + diskCacheSize?: number; electronVersionUsed?: string; enableEs3Apis: boolean; fastQuit: boolean; - fileDownloadOptions: any; - flashPluginDir: string; + fileDownloadOptions: unknown; + flashPluginDir?: string; fullScreen: boolean; - globalShortcuts: any; + globalShortcuts?: GlobalShortcut[]; hideWindowFrame: boolean; ignoreCertificate: boolean; ignoreGpuBlacklist: boolean; - inject: string[]; + inject?: string[]; insecure: boolean; - internalUrls: string; + internalUrls?: string; lang?: string; maximize: boolean; - nativefierVersion: string; - processEnvs: string; - proxyRules: string; + nativefierVersion?: string; + processEnvs?: string; + proxyRules?: string; showMenuBar: boolean; singleInstance: boolean; - titleBarStyle: string; + titleBarStyle?: string; tray: string | boolean; - userAgent: string; + userAgent?: string; userAgentHonest: boolean; verbose: boolean; - versionString: string; - width: number; + versionString?: string; + width?: number; widevine: boolean; - height: number; - minWidth: number; - minHeight: number; - maxWidth: number; - maxHeight: number; - x: number; - y: number; + height?: number; + minWidth?: number; + minHeight?: number; + maxWidth?: number; + maxHeight?: number; + x?: number; + y?: number; zoom: number; }; } +export type BrowserWindowOptions = Record<string, unknown>; + +export type GlobalShortcut = { + key: string; +}; + export type NativefierOptions = Partial< AppOptions['packager'] & AppOptions['nativefier'] >; + +export type OutputOptions = NativefierOptions & { + buildDate: number; + isUpgrade: boolean; + oldBuildWarningText: string; +}; + +export type PackageJSON = { + name: string; +}; + +export type RawOptions = { + accessibilityPrompt?: boolean; + alwaysOnTop?: boolean; + appCopyright?: string; + appVersion?: string; + arch?: string | string[]; + asar?: boolean | CreateOptions; + backgroundColor?: string; + basicAuthPassword?: string; + basicAuthUsername?: string; + blockExternalUrls?: boolean; + bookmarksMenu?: string; + bounce?: boolean; + browserwindowOptions?: BrowserWindowOptions; + buildVersion?: string; + clearCache?: boolean; + conceal?: boolean; + counter?: boolean; + crashReporter?: string; + darwinDarkModeSupport?: boolean; + disableContextMenu?: boolean; + disableDevTools?: boolean; + disableGpu?: boolean; + disableOldBuildWarning?: boolean; + disableOldBuildWarningYesiknowitisinsecure?: boolean; + diskCacheSize?: number; + electronVersion?: string; + electronVersionUsed?: string; + enableEs3Apis?: boolean; + fastQuit?: boolean; + fileDownloadOptions?: unknown; + flashPath?: string; + flashPluginDir?: string; + fullScreen?: boolean; + globalShortcuts?: string | GlobalShortcut[]; + height?: number; + hideWindowFrame?: boolean; + icon?: string; + ignoreCertificate?: boolean; + ignoreGpuBlacklist?: boolean; + inject?: string[]; + insecure?: boolean; + internalUrls?: string; + lang?: string; + maxHeight?: number; + maximize?: boolean; + maxWidth?: number; + minHeight?: number; + minWidth?: number; + name?: string; + nativefierVersion?: string; + out?: string; + overwrite?: boolean; + platform?: string; + portable?: boolean; + processEnvs?: string; + proxyRules?: string; + showMenuBar?: boolean; + singleInstance?: boolean; + targetUrl?: string; + titleBarStyle?: string; + tray?: string | boolean; + upgrade?: string | boolean; + upgradeFrom?: string; + userAgent?: string; + userAgentHonest?: boolean; + verbose?: boolean; + versionString?: string; + widevine?: boolean; + width?: number; + win32metadata?: electronPackager.Win32MetadataOptions; + x?: number; + y?: number; + zoom?: number; +}; diff --git a/src/options/normalizeUrl.ts b/src/options/normalizeUrl.ts index e513b19..054bc25 100644 --- a/src/options/normalizeUrl.ts +++ b/src/options/normalizeUrl.ts @@ -22,8 +22,9 @@ export function normalizeUrl(urlToNormalize: string): string { let parsedUrl: url.URL; try { parsedUrl = new url.URL(urlWithProtocol); - } catch (err) { - throw `Your url "${urlWithProtocol}" is invalid`; + } catch (err: unknown) { + log.error('normalizeUrl ERROR', err); + throw new Error(`Your url "${urlWithProtocol}" is invalid`); } const normalizedUrl = parsedUrl.toString(); log.debug(`Normalized URL ${urlToNormalize} to:`, normalizedUrl); diff --git a/src/options/optionsMain.test.ts b/src/options/optionsMain.test.ts index b36139d..c76eb26 100644 --- a/src/options/optionsMain.test.ts +++ b/src/options/optionsMain.test.ts @@ -1,9 +1,71 @@ import { getOptions, normalizePlatform } from './optionsMain'; import * as asyncConfig from './asyncConfig'; import { inferPlatform } from '../infer/inferOs'; +import { AppOptions } from './model'; -const mockedAsyncConfig = { some: 'options' }; let asyncConfigMock: jest.SpyInstance; +const mockedAsyncConfig: AppOptions = { + nativefier: { + accessibilityPrompt: false, + alwaysOnTop: false, + backgroundColor: undefined, + basicAuthPassword: undefined, + basicAuthUsername: undefined, + blockExternalUrls: false, + bookmarksMenu: undefined, + bounce: false, + browserwindowOptions: undefined, + clearCache: false, + counter: false, + crashReporter: undefined, + disableContextMenu: false, + disableDevTools: false, + disableGpu: false, + disableOldBuildWarning: false, + diskCacheSize: undefined, + enableEs3Apis: false, + fastQuit: true, + fileDownloadOptions: undefined, + flashPluginDir: undefined, + fullScreen: false, + globalShortcuts: undefined, + height: undefined, + hideWindowFrame: false, + ignoreCertificate: false, + ignoreGpuBlacklist: false, + inject: [], + insecure: false, + internalUrls: undefined, + maximize: false, + maxHeight: undefined, + minWidth: undefined, + minHeight: undefined, + maxWidth: undefined, + nativefierVersion: '1.0.0', + processEnvs: undefined, + proxyRules: undefined, + showMenuBar: false, + singleInstance: false, + titleBarStyle: undefined, + tray: false, + userAgent: undefined, + userAgentHonest: false, + verbose: false, + versionString: '1.0.0', + width: undefined, + widevine: false, + x: undefined, + y: undefined, + zoom: 1, + }, + packager: { + dir: '', + platform: process.platform, + portable: false, + targetUrl: '', + upgrade: false, + }, +}; beforeAll(() => { asyncConfigMock = jest @@ -18,8 +80,8 @@ test('it should call the async config', async () => { const result = await getOptions(params); expect(asyncConfigMock).toHaveBeenCalledWith( expect.objectContaining({ - packager: expect.anything(), - nativefier: expect.anything(), + packager: expect.anything() as AppOptions['packager'], + nativefier: expect.anything() as AppOptions['nativefier'], }), ); expect(result.packager.targetUrl).toEqual(params.targetUrl); @@ -34,7 +96,7 @@ test('it should set the accessibility prompt option to true by default', async ( expect.objectContaining({ nativefier: expect.objectContaining({ accessibilityPrompt: true, - }), + }) as AppOptions['nativefier'], }), ); expect(result.nativefier.accessibilityPrompt).toEqual(true); diff --git a/src/options/optionsMain.ts b/src/options/optionsMain.ts index 87844b1..ad7b13e 100644 --- a/src/options/optionsMain.ts +++ b/src/options/optionsMain.ts @@ -1,11 +1,17 @@ import * as fs from 'fs'; import axios from 'axios'; +import * as debug from 'debug'; import * as log from 'loglevel'; // package.json is `require`d to let tsc strip the `src` folder by determining // baseUrl=src. A static import would prevent that and cause an ugly extra `src` folder in `lib` -const packageJson = require('../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const packageJson: { + name: string; + version: string; + // eslint-disable-next-line @typescript-eslint/no-var-requires +} = require('../../package.json'); import { DEFAULT_ELECTRON_VERSION, PLACEHOLDER_APP_DIR, @@ -13,8 +19,9 @@ import { } from '../constants'; import { inferPlatform, inferArch } from '../infer/inferOs'; import { asyncConfig } from './asyncConfig'; -import { AppOptions } from './model'; +import { AppOptions, GlobalShortcut, RawOptions } from './model'; import { normalizeUrl } from './normalizeUrl'; +import { parseJson } from '../utils/parseUtils'; const SEMVER_VERSION_NUMBER_REGEX = /\d+\.\d+\.\d+[-_\w\d.]*/; @@ -22,31 +29,33 @@ const SEMVER_VERSION_NUMBER_REGEX = /\d+\.\d+\.\d+[-_\w\d.]*/; * Process and validate raw user arguments */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export async function getOptions(rawOptions: any): Promise<AppOptions> { +export async function getOptions(rawOptions: RawOptions): Promise<AppOptions> { const options: AppOptions = { packager: { appCopyright: rawOptions.appCopyright, appVersion: rawOptions.appVersion, - arch: rawOptions.arch || inferArch(), - asar: rawOptions.asar || rawOptions.conceal || false, + arch: rawOptions.arch ?? inferArch(), + asar: rawOptions.asar ?? rawOptions.conceal ?? false, buildVersion: rawOptions.buildVersion, - darwinDarkModeSupport: rawOptions.darwinDarkModeSupport || false, + darwinDarkModeSupport: rawOptions.darwinDarkModeSupport ?? false, dir: PLACEHOLDER_APP_DIR, - electronVersion: rawOptions.electronVersion || DEFAULT_ELECTRON_VERSION, + electronVersion: rawOptions.electronVersion ?? DEFAULT_ELECTRON_VERSION, icon: rawOptions.icon, name: typeof rawOptions.name === 'string' ? rawOptions.name : '', - out: rawOptions.out || process.cwd(), + out: rawOptions.out ?? process.cwd(), overwrite: rawOptions.overwrite, platform: rawOptions.platform, - portable: rawOptions.portable || false, + portable: rawOptions.portable ?? false, targetUrl: rawOptions.targetUrl === undefined ? '' // We'll plug this in later via upgrade : normalizeUrl(rawOptions.targetUrl), tmpdir: false, // workaround for electron-packager#375 upgrade: rawOptions.upgrade !== undefined ? true : false, - upgradeFrom: rawOptions.upgrade, - win32metadata: rawOptions.win32metadata || { + upgradeFrom: + (rawOptions.upgradeFrom as string) ?? + ((rawOptions.upgrade as string) || undefined), + win32metadata: rawOptions.win32metadata ?? { ProductName: rawOptions.name, InternalName: rawOptions.name, FileDescription: rawOptions.name, @@ -54,70 +63,70 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> { }, nativefier: { accessibilityPrompt: true, - alwaysOnTop: rawOptions.alwaysOnTop || false, - backgroundColor: rawOptions.backgroundColor || null, - basicAuthPassword: rawOptions.basicAuthPassword || null, - basicAuthUsername: rawOptions.basicAuthUsername || null, - blockExternalUrls: rawOptions.blockExternalUrls || false, - bookmarksMenu: rawOptions.bookmarksMenu || null, - bounce: rawOptions.bounce || false, + alwaysOnTop: rawOptions.alwaysOnTop ?? false, + backgroundColor: rawOptions.backgroundColor, + basicAuthPassword: rawOptions.basicAuthPassword, + basicAuthUsername: rawOptions.basicAuthUsername, + blockExternalUrls: rawOptions.blockExternalUrls ?? false, + bookmarksMenu: rawOptions.bookmarksMenu, + bounce: rawOptions.bounce ?? false, browserwindowOptions: rawOptions.browserwindowOptions, - clearCache: rawOptions.clearCache || false, - counter: rawOptions.counter || false, + clearCache: rawOptions.clearCache ?? false, + counter: rawOptions.counter ?? false, crashReporter: rawOptions.crashReporter, - disableContextMenu: rawOptions.disableContextMenu, - disableDevTools: rawOptions.disableDevTools, - disableGpu: rawOptions.disableGpu || false, - diskCacheSize: rawOptions.diskCacheSize || null, + disableContextMenu: rawOptions.disableContextMenu ?? false, + disableDevTools: rawOptions.disableDevTools ?? false, + disableGpu: rawOptions.disableGpu ?? false, + diskCacheSize: rawOptions.diskCacheSize, disableOldBuildWarning: - rawOptions.disableOldBuildWarningYesiknowitisinsecure || false, - enableEs3Apis: rawOptions.enableEs3Apis || false, - fastQuit: rawOptions.fastQuit || false, + rawOptions.disableOldBuildWarningYesiknowitisinsecure ?? false, + enableEs3Apis: rawOptions.enableEs3Apis ?? false, + fastQuit: rawOptions.fastQuit ?? false, fileDownloadOptions: rawOptions.fileDownloadOptions, - flashPluginDir: rawOptions.flashPath || rawOptions.flash || null, - fullScreen: rawOptions.fullScreen || false, - globalShortcuts: null, - hideWindowFrame: rawOptions.hideWindowFrame, - ignoreCertificate: rawOptions.ignoreCertificate || false, - ignoreGpuBlacklist: rawOptions.ignoreGpuBlacklist || false, - inject: rawOptions.inject || [], - insecure: rawOptions.insecure || false, - internalUrls: rawOptions.internalUrls || null, - lang: rawOptions.lang || undefined, - maximize: rawOptions.maximize || false, + flashPluginDir: rawOptions.flashPath, + fullScreen: rawOptions.fullScreen ?? false, + globalShortcuts: undefined, + hideWindowFrame: rawOptions.hideWindowFrame ?? false, + ignoreCertificate: rawOptions.ignoreCertificate ?? false, + ignoreGpuBlacklist: rawOptions.ignoreGpuBlacklist ?? false, + inject: rawOptions.inject ?? [], + insecure: rawOptions.insecure ?? false, + internalUrls: rawOptions.internalUrls, + lang: rawOptions.lang, + maximize: rawOptions.maximize ?? false, nativefierVersion: packageJson.version, processEnvs: rawOptions.processEnvs, - proxyRules: rawOptions.proxyRules || null, - showMenuBar: rawOptions.showMenuBar || false, - singleInstance: rawOptions.singleInstance || false, - titleBarStyle: rawOptions.titleBarStyle || null, - tray: rawOptions.tray || false, + proxyRules: rawOptions.proxyRules, + showMenuBar: rawOptions.showMenuBar ?? false, + singleInstance: rawOptions.singleInstance ?? false, + titleBarStyle: rawOptions.titleBarStyle, + tray: rawOptions.tray ?? false, userAgent: rawOptions.userAgent, - userAgentHonest: rawOptions.userAgentHonest || false, - verbose: rawOptions.verbose, + userAgentHonest: rawOptions.userAgentHonest ?? false, + verbose: rawOptions.verbose ?? false, versionString: rawOptions.versionString, - width: rawOptions.width || 1280, - height: rawOptions.height || 800, + width: rawOptions.width ?? 1280, + height: rawOptions.height ?? 800, minWidth: rawOptions.minWidth, minHeight: rawOptions.minHeight, maxWidth: rawOptions.maxWidth, maxHeight: rawOptions.maxHeight, - widevine: rawOptions.widevine || false, + widevine: rawOptions.widevine ?? false, x: rawOptions.x, y: rawOptions.y, - zoom: rawOptions.zoom || 1.0, + zoom: rawOptions.zoom ?? 1.0, }, }; if (options.nativefier.verbose) { log.setLevel('trace'); try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('debug').enable('electron-packager'); - } catch (err) { - log.debug( + debug.enable('electron-packager'); + } catch (err: unknown) { + log.error( 'Failed to enable electron-packager debug output. This should not happen,', 'and suggests their internals changed. Please report an issue.', + err, ); } @@ -145,13 +154,17 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> { } if (options.nativefier.widevine) { - const widevineElectronVersion = `${options.packager.electronVersion}-wvvmp`; + const widevineElectronVersion = `${ + options.packager.electronVersion as string + }-wvvmp`; try { await axios.get( `https://github.com/castlabs/electron-releases/releases/tag/v${widevineElectronVersion}`, ); - } catch (error) { - throw `\nERROR: castLabs Electron version "${widevineElectronVersion}" does not exist. \nVerify versions at https://github.com/castlabs/electron-releases/releases. \nAborting.`; + } catch { + throw new Error( + `\nERROR: castLabs Electron version "${widevineElectronVersion}" does not exist. \nVerify versions at https://github.com/castlabs/electron-releases/releases. \nAborting.`, + ); } options.packager.electronVersion = widevineElectronVersion; @@ -173,7 +186,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> { } if (options.nativefier.userAgentHonest && options.nativefier.userAgent) { - options.nativefier.userAgent = null; + options.nativefier.userAgent = undefined; log.warn( `\nATTENTION: user-agent AND user-agent-honest/honest were provided. In this case, honesty wins. user-agent will be ignored`, ); @@ -181,11 +194,19 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> { options.packager.platform = normalizePlatform(options.packager.platform); - if (options.nativefier.width > options.nativefier.maxWidth) { + if ( + options.nativefier.maxWidth && + options.nativefier.width && + options.nativefier.width > options.nativefier.maxWidth + ) { options.nativefier.width = options.nativefier.maxWidth; } - if (options.nativefier.height > options.nativefier.maxHeight) { + if ( + options.nativefier.maxHeight && + options.nativefier.height && + options.nativefier.height > options.nativefier.maxHeight + ) { options.nativefier.height = options.nativefier.maxHeight; } @@ -202,8 +223,8 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> { if (rawOptions.globalShortcuts) { log.debug('Using global shortcuts file at', rawOptions.globalShortcuts); - const globalShortcuts = JSON.parse( - fs.readFileSync(rawOptions.globalShortcuts).toString(), + const globalShortcuts = parseJson<GlobalShortcut[]>( + fs.readFileSync(rawOptions.globalShortcuts as string).toString(), ); options.nativefier.globalShortcuts = globalShortcuts; } @@ -213,7 +234,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> { return options; } -export function normalizePlatform(platform: string): string { +export function normalizePlatform(platform: string | undefined): string { if (!platform) { return inferPlatform(); } diff --git a/src/utils/parseUtils.test.ts b/src/utils/parseUtils.test.ts index 4f80383..990d685 100644 --- a/src/utils/parseUtils.test.ts +++ b/src/utils/parseUtils.test.ts @@ -14,8 +14,12 @@ test.each([ [undefined, true, true], [undefined, false, false], ])( - 'parseBoolean("%s") === %s', - (testString: string, expectedResult: boolean, _default: boolean) => { - expect(parseBoolean(testString, _default)).toBe(expectedResult); + 'parseBoolean("%s") === %s (default = %s)', + ( + testValue: boolean | string | number | undefined, + expectedResult: boolean, + _default: boolean, + ) => { + expect(parseBoolean(testValue, _default)).toBe(expectedResult); }, ); diff --git a/src/utils/parseUtils.ts b/src/utils/parseUtils.ts index 390c9f7..a853a62 100644 --- a/src/utils/parseUtils.ts +++ b/src/utils/parseUtils.ts @@ -3,9 +3,12 @@ import * as log from 'loglevel'; import { isWindows } from '../helpers/helpers'; export function parseBoolean( - val: boolean | string | number, + val: boolean | string | number | undefined, _default: boolean, ): boolean { + if (val === undefined) { + return _default; + } try { if (typeof val === 'boolean') { return val; @@ -23,7 +26,7 @@ export function parseBoolean( default: return _default; } - } catch (_) { + } catch { return _default; } } @@ -39,11 +42,11 @@ export function parseBooleanOrString(val: string): boolean | string { } } -export function parseJson(val: string): any { - if (!val) return {}; +export function parseJson<Type>(val: string): Type | undefined { + if (!val) return undefined; try { - return JSON.parse(val); - } catch (err) { + return JSON.parse(val) as Type; + } catch (err: unknown) { const windowsShellHint = isWindows() ? `\n In particular, Windows cmd doesn't have single quotes, so you have to use only double-quotes plus escaping: "{\\"someKey\\": \\"someValue\\"}"` : ''; diff --git a/src/utils/sanitizeFilename.ts b/src/utils/sanitizeFilename.ts index bde0684..d52e81f 100644 --- a/src/utils/sanitizeFilename.ts +++ b/src/utils/sanitizeFilename.ts @@ -4,7 +4,7 @@ import sanitize = require('sanitize-filename'); import { DEFAULT_APP_NAME } from '../constants'; export function sanitizeFilename( - platform: string, + platform: string | undefined, filenameToSanitize: string, ): string { let result: string = sanitize(filenameToSanitize); diff --git a/tsconfig.json b/tsconfig.json index f20b41d..e0604bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,32 +1,36 @@ { "compilerOptions": { - "allowJs": false, - "declaration": true, - "incremental": true, - "module": "commonjs", - "moduleResolution": "node", - "outDir": "./lib", - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - // Bumping the minimum required Node version? You must bump: - // 1. package.json -> engines.node - // 2. package.json -> devDependencies.@types/node - // 3. tsconfig.json -> {target, lib} - // 4. .github/workflows/ci.yml -> node-version - // - // Here in tsconfig.json, we want to set the `target` and `lib` keys - // to the "best" values for the minimum/required version of node. - // TS doesn't offer any easy "preset" for this, so the best we have is to - // believe people who know which {syntax, library} parts of current EcmaScript - // are supported for our version of Node, and use what they recommend. - // For the current Node version, I followed - // https://stackoverflow.com/questions/59787574/typescript-tsconfig-settings-for-node-js-12 - "target": "es2019", - // In `lib` we add `dom`, to tell tsc it's okay to use the URL object (which is in Node >= 7) - "lib": ["es2020", "dom"] + "allowJs": false, + "declaration": true, + "incremental": true, + "module": "commonjs", + "moduleResolution": "node", + "outDir": "./lib", + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + // Bumping the minimum required Node version? You must bump: + // 1. package.json -> engines.node + // 2. package.json -> devDependencies.@types/node + // 3. tsconfig.json -> {target, lib} + // 4. .github/workflows/ci.yml -> node-version + // + // Here in tsconfig.json, we want to set the `target` and `lib` keys + // to the "best" values for the minimum/required version of node. + // TS doesn't offer any easy "preset" for this, so the best we have is to + // believe people who know which {syntax, library} parts of current EcmaScript + // are supported for our version of Node, and use what they recommend. + // For the current Node version, I followed + // https://stackoverflow.com/questions/59787574/typescript-tsconfig-settings-for-node-js-12 + "target": "es2019", + // In `lib` we add `dom`, to tell tsc it's okay to use the URL object (which is in Node >= 7) + "lib": [ + "es2020", + "dom" + ] }, "include": [ - "./src/**/*" + "./src/**/*" ] }