mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2024-12-22 18:18:55 +00:00
Enable TypeScript strict:true, and more typescript-eslint rules (#1223)
* Catch promise errors better * Move subFunctions to bottom of createNewWindow * Use parents when creating child BrowserWindow instances * Some about:blank pages have an anchor (for some reason) * Inject browserWindowOptions better * Interim refactor to MainWindow object * Split up the window functions/helpers/events some * Further separate out window functions + tests * Add a mock for unit testing functions that access electron * Add unit tests for onWillPreventUnload * Improve windowEvents tests * Add the first test for windowHelpers * Move WebRequest event handling to node * insertCSS completely under test * clearAppData completely under test * Fix contextMenu require bug * More tests + fixes * Fix + add to createNewTab tests * Convert createMainWindow back to func + work out gremlins * Move setupWindow away from main since its shared * Make sure contextMenu is handling promises * Fix issues with fullscreen not working + menu refactor * Run jest against app/dist so that we can hit app unit tests as well * Requested PR changes * Add strict typing; tests currently failing * Fix failing unit tests * Add some more eslint warnings and fixes * More eslint fixes * Strict typing on (still issues with the lib dir) * Fix the package.json import/require * Fix some funky test errors * Warn -> Error for eslint rules * @ts-ignore -> @ts-expect-error * Add back the ext code I removed
This commit is contained in:
parent
7807bbb327
commit
7a08a2d676
38
.eslintrc.js
38
.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/**',
|
||||
],
|
||||
};
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -60,3 +60,5 @@ nativefier*.tgz
|
||||
|
||||
# https://github.com/nektos/act
|
||||
.actrc
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
|
@ -25,14 +25,14 @@ export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
||||
type SessionInteractionRequest = {
|
||||
id?: string;
|
||||
func?: string;
|
||||
funcArgs?: any[];
|
||||
funcArgs?: unknown[];
|
||||
property?: string;
|
||||
propertyValue?: any;
|
||||
propertyValue?: unknown;
|
||||
};
|
||||
|
||||
type SessionInteractionResult = {
|
||||
id?: string;
|
||||
value?: any;
|
||||
value?: unknown;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
@ -144,20 +144,17 @@ function createContextMenu(options, window: BrowserWindow): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAppArgs(newAppArgs: any) {
|
||||
export function saveAppArgs(newAppArgs: any): void {
|
||||
try {
|
||||
fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
} catch (err: unknown) {
|
||||
log.warn(
|
||||
`WARNING: Ignored nativefier.json rewrital (${(
|
||||
err as Error
|
||||
).toString()})`,
|
||||
`WARNING: Ignored nativefier.json rewrital (${(err as Error).message})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setupCloseEvent(options, window: BrowserWindow) {
|
||||
function setupCloseEvent(options, window: BrowserWindow): void {
|
||||
window.on('close', (event: IpcMainEvent) => {
|
||||
log.debug('mainWindow.close', event);
|
||||
if (window.isFullScreen()) {
|
||||
@ -181,7 +178,7 @@ function setupCounter(
|
||||
options,
|
||||
window: BrowserWindow,
|
||||
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
||||
) {
|
||||
): void {
|
||||
window.on('page-title-updated', (event, title) => {
|
||||
log.debug('mainWindow.page-title-updated', { event, title });
|
||||
const counterValue = getCounterValue(title);
|
||||
@ -242,7 +239,7 @@ function setupSessionInteraction(options, window: BrowserWindow): void {
|
||||
typeof result.value['then'] === 'function'
|
||||
) {
|
||||
// This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply
|
||||
result.value
|
||||
(result.value as Promise<unknown>)
|
||||
.then((trueResultValue) => {
|
||||
result.value = trueResultValue;
|
||||
log.debug('ipcMain.session-interaction:result', result);
|
||||
@ -274,9 +271,9 @@ function setupSessionInteraction(options, window: BrowserWindow): void {
|
||||
log.debug('session-interaction:result', result);
|
||||
event.reply('session-interaction-reply', result);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('session-interaction:error', error, event, request);
|
||||
result.error = error;
|
||||
} catch (err: unknown) {
|
||||
log.error('session-interaction:error', err, event, request);
|
||||
result.error = err as Error;
|
||||
result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place
|
||||
event.reply('session-interaction-reply', result);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export function createTrayIcon(
|
||||
appIcon.setImage(nimage);
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
const onClick = (): void => {
|
||||
log.debug('onClick');
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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));
|
||||
});
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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',
|
||||
{
|
||||
|
@ -83,7 +83,7 @@ export function createNewTab(
|
||||
url: string,
|
||||
foreground: boolean,
|
||||
parent?: BrowserWindow,
|
||||
): Promise<BrowserWindow> {
|
||||
): BrowserWindow {
|
||||
log.debug('createNewTab', { url, foreground, parent });
|
||||
return withFocusedWindow((focusedWindow) => {
|
||||
const newTab = createNewWindow(options, setupWindow, url, parent);
|
||||
@ -314,7 +314,7 @@ export function setProxyRules(window: BrowserWindow, proxyRules): void {
|
||||
.catch((err) => log.error('session.setProxy ERROR', err));
|
||||
}
|
||||
|
||||
export function withFocusedWindow(block: (window: BrowserWindow) => any): any {
|
||||
export function withFocusedWindow<T>(block: (window: BrowserWindow) => T): T {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
return block(focusedWindow);
|
||||
|
@ -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 });
|
||||
},
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
@ -21,12 +22,12 @@ import { EventEmitter } from 'events';
|
||||
class MockBrowserWindow extends EventEmitter {
|
||||
webContents: MockWebContents;
|
||||
|
||||
constructor(options?: any) {
|
||||
constructor(options?: unknown) {
|
||||
super(options);
|
||||
this.webContents = new MockWebContents();
|
||||
}
|
||||
|
||||
addTabbedWindow(tab: MockBrowserWindow) {
|
||||
addTabbedWindow(tab: MockBrowserWindow): void {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -54,7 +55,7 @@ class MockBrowserWindow extends EventEmitter {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
loadURL(url: string, options?: any): Promise<void> {
|
||||
loadURL(url: string, options?: unknown): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
@ -70,14 +71,14 @@ class MockBrowserWindow extends EventEmitter {
|
||||
class MockDialog {
|
||||
static showMessageBox(
|
||||
browserWindow: MockBrowserWindow,
|
||||
options: any,
|
||||
options: unknown,
|
||||
): Promise<number> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
static showMessageBoxSync(
|
||||
browserWindow: MockBrowserWindow,
|
||||
options: any,
|
||||
options: unknown,
|
||||
): number {
|
||||
return undefined;
|
||||
}
|
||||
@ -112,7 +113,7 @@ class MockWebContents extends EventEmitter {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
insertCSS(css: string, options?: any): Promise<string> {
|
||||
insertCSS(css: string, options?: unknown): Promise<string> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
@ -125,22 +126,24 @@ class MockWebRequest {
|
||||
}
|
||||
|
||||
onHeadersReceived(
|
||||
filter: any,
|
||||
filter: unknown,
|
||||
listener:
|
||||
| ((
|
||||
details: any,
|
||||
callback: (headersReceivedResponse: any) => void,
|
||||
details: unknown,
|
||||
callback: (headersReceivedResponse: unknown) => void,
|
||||
) => void)
|
||||
| null,
|
||||
): void {
|
||||
this.emitter.addListener(
|
||||
'onHeadersReceived',
|
||||
(details: any, callback: (headersReceivedResponse: any) => void) =>
|
||||
listener(details, callback),
|
||||
(
|
||||
details: unknown,
|
||||
callback: (headersReceivedResponse: unknown) => void,
|
||||
) => listener(details, callback),
|
||||
);
|
||||
}
|
||||
|
||||
send(event: string, ...args: any[]): void {
|
||||
send(event: string, ...args: unknown[]): void {
|
||||
this.emitter.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -2,18 +2,18 @@ import * as path from 'path';
|
||||
|
||||
import * as electronGet from '@electron/get';
|
||||
import * as electronPackager from 'electron-packager';
|
||||
import * as hasbin from 'hasbin';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { convertIconIfNecessary } from './buildIcon';
|
||||
import {
|
||||
copyFileOrDir,
|
||||
getTempDir,
|
||||
hasWine,
|
||||
isWindows,
|
||||
isWindowsAdmin,
|
||||
} from '../helpers/helpers';
|
||||
import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade';
|
||||
import { AppOptions } from '../options/model';
|
||||
import { AppOptions, RawOptions } from '../options/model';
|
||||
import { getOptions } from '../options/optionsMain';
|
||||
import { prepareElectronApp } from './prepareElectronApp';
|
||||
|
||||
@ -70,13 +70,13 @@ async function copyIconsIfNecessary(
|
||||
/**
|
||||
* Checks the app path array to determine if packaging completed successfully
|
||||
*/
|
||||
function getAppPath(appPath: string | string[]): string {
|
||||
function getAppPath(appPath: string | string[]): string | undefined {
|
||||
if (!Array.isArray(appPath)) {
|
||||
return appPath;
|
||||
}
|
||||
|
||||
if (appPath.length === 0) {
|
||||
return null; // directory already exists and `--overwrite` not set
|
||||
return undefined; // directory already exists and `--overwrite` not set
|
||||
}
|
||||
|
||||
if (appPath.length > 1) {
|
||||
@ -89,20 +89,21 @@ function getAppPath(appPath: string | string[]): string {
|
||||
return appPath[0];
|
||||
}
|
||||
|
||||
function isUpgrade(rawOptions) {
|
||||
return (
|
||||
function isUpgrade(rawOptions: RawOptions): boolean {
|
||||
if (
|
||||
rawOptions.upgrade !== undefined &&
|
||||
(rawOptions.upgrade === true ||
|
||||
(typeof rawOptions.upgrade === 'string' && rawOptions.upgrade !== ''))
|
||||
);
|
||||
typeof rawOptions.upgrade === 'string' &&
|
||||
rawOptions.upgrade !== ''
|
||||
) {
|
||||
rawOptions.upgradeFrom = rawOptions.upgrade;
|
||||
rawOptions.upgrade = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function trimUnprocessableOptions(options: AppOptions): void {
|
||||
if (
|
||||
options.packager.platform === 'win32' &&
|
||||
!isWindows() &&
|
||||
!hasbin.sync('wine')
|
||||
) {
|
||||
if (options.packager.platform === 'win32' && !isWindows() && !hasWine()) {
|
||||
const optionsPresent = Object.entries(options)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
@ -119,28 +120,30 @@ function trimUnprocessableOptions(options: AppOptions): void {
|
||||
'features, like a correct icon and process name. Do yourself a favor and install Wine, please.',
|
||||
);
|
||||
for (const keyToUnset of optionsPresent) {
|
||||
options[keyToUnset] = null;
|
||||
(options as unknown as Record<string, undefined>)[keyToUnset] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export async function buildNativefierApp(rawOptions): Promise<string> {
|
||||
export async function buildNativefierApp(
|
||||
rawOptions: RawOptions,
|
||||
): Promise<string | undefined> {
|
||||
log.info('\nProcessing options...');
|
||||
|
||||
if (isUpgrade(rawOptions)) {
|
||||
log.debug('Attempting to upgrade from', rawOptions.upgrade);
|
||||
const oldApp = findUpgradeApp(rawOptions.upgrade.toString());
|
||||
log.debug('Attempting to upgrade from', rawOptions.upgradeFrom);
|
||||
const oldApp = findUpgradeApp(rawOptions.upgradeFrom as string);
|
||||
if (oldApp === null) {
|
||||
throw new Error(
|
||||
`Could not find an old Nativfier app in "${
|
||||
rawOptions.upgrade as string
|
||||
rawOptions.upgradeFrom as string
|
||||
}"`,
|
||||
);
|
||||
}
|
||||
rawOptions = useOldAppOptions(rawOptions, oldApp);
|
||||
if (rawOptions.out === undefined && rawOptions.overwrite) {
|
||||
rawOptions.out = path.dirname(rawOptions.upgrade);
|
||||
rawOptions.out = path.dirname(rawOptions.upgradeFrom as string);
|
||||
}
|
||||
}
|
||||
log.debug('rawOptions', rawOptions);
|
||||
|
@ -6,14 +6,15 @@ import { promisify } from 'util';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { copyFileOrDir, generateRandomSuffix } from '../helpers/helpers';
|
||||
import { AppOptions } from '../options/model';
|
||||
import { AppOptions, OutputOptions, PackageJSON } from '../options/model';
|
||||
import { parseJson } from '../utils/parseUtils';
|
||||
|
||||
const writeFileAsync = promisify(fs.writeFile);
|
||||
|
||||
/**
|
||||
* Only picks certain app args to pass to nativefier.json
|
||||
*/
|
||||
function pickElectronAppArgs(options: AppOptions): any {
|
||||
function pickElectronAppArgs(options: AppOptions): OutputOptions {
|
||||
return {
|
||||
accessibilityPrompt: options.nativefier.accessibilityPrompt,
|
||||
alwaysOnTop: options.nativefier.alwaysOnTop,
|
||||
@ -99,7 +100,10 @@ function pickElectronAppArgs(options: AppOptions): any {
|
||||
};
|
||||
}
|
||||
|
||||
async function maybeCopyScripts(srcs: string[], dest: string): Promise<void> {
|
||||
async function maybeCopyScripts(
|
||||
srcs: string[] | undefined,
|
||||
dest: string,
|
||||
): Promise<void> {
|
||||
if (!srcs || srcs.length === 0) {
|
||||
log.debug('No files to inject, skipping copy.');
|
||||
return;
|
||||
@ -151,7 +155,12 @@ function changeAppPackageJsonName(
|
||||
url: string,
|
||||
): void {
|
||||
const packageJsonPath = path.join(appPath, '/package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString());
|
||||
const packageJson = parseJson<PackageJSON>(
|
||||
fs.readFileSync(packageJsonPath).toString(),
|
||||
);
|
||||
if (!packageJson) {
|
||||
throw new Error(`Could not load package.json from ${packageJsonPath}`);
|
||||
}
|
||||
const normalizedAppName = normalizeAppName(name, url);
|
||||
packageJson.name = normalizedAppName;
|
||||
log.debug(`Updating ${packageJsonPath} 'name' field to ${normalizedAppName}`);
|
||||
@ -171,10 +180,10 @@ export async function prepareElectronApp(
|
||||
log.debug(`Copying electron app from ${src} to ${dest}`);
|
||||
try {
|
||||
await copyFileOrDir(src, dest);
|
||||
} catch (err) {
|
||||
throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${(
|
||||
err as Error
|
||||
).toString()}`;
|
||||
} catch (err: unknown) {
|
||||
throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${
|
||||
(err as Error).message
|
||||
}`;
|
||||
}
|
||||
|
||||
const appJsonPath = path.join(dest, '/nativefier.json');
|
||||
@ -188,19 +197,19 @@ export async function prepareElectronApp(
|
||||
const bookmarksJsonPath = path.join(dest, '/bookmarks.json');
|
||||
try {
|
||||
await copyFileOrDir(options.nativefier.bookmarksMenu, bookmarksJsonPath);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
log.error('Error copying bookmarks menu config file.', err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await maybeCopyScripts(options.nativefier.inject, dest);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
log.error('Error copying injection files.', err);
|
||||
}
|
||||
changeAppPackageJsonName(
|
||||
dest,
|
||||
options.packager.name,
|
||||
options.packager.name as string,
|
||||
options.packager.targetUrl,
|
||||
);
|
||||
}
|
||||
|
164
src/cli.test.ts
164
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<string, string>;
|
||||
if (!isJsonString) {
|
||||
expect(args[arg]).toBe(value);
|
||||
} else {
|
||||
@ -147,8 +149,8 @@ describe('initArgs + parseArgs', () => {
|
||||
|
||||
if (shortArg) {
|
||||
const argsShort = parseArgs(
|
||||
initArgs(['https://google.com', `-${shortArg as string}`, value]),
|
||||
);
|
||||
initArgs(['https://google.com', `-${shortArg}`, value]),
|
||||
) as Record<string, string>;
|
||||
if (!isJsonString) {
|
||||
expect(argsShort[arg]).toBe(value);
|
||||
} else {
|
||||
@ -162,12 +164,14 @@ describe('initArgs + parseArgs', () => {
|
||||
{ arg: 'platform', shortArg: 'p', value: 'mac', badValue: 'os2' },
|
||||
{
|
||||
arg: 'title-bar-style',
|
||||
shortArg: null,
|
||||
shortArg: '',
|
||||
value: 'hidden',
|
||||
badValue: 'cool',
|
||||
},
|
||||
])('limited choice arg %s', ({ arg, shortArg, value, badValue }) => {
|
||||
const args = parseArgs(initArgs(['https://google.com', `--${arg}`, value]));
|
||||
const args = parseArgs(
|
||||
initArgs(['https://google.com', `--${arg}`, value]),
|
||||
) as Record<string, string>;
|
||||
expect(args[arg]).toBe(value);
|
||||
|
||||
// Mock console.error to not pollute the log with the yargs help text
|
||||
@ -181,7 +185,7 @@ describe('initArgs + parseArgs', () => {
|
||||
if (shortArg) {
|
||||
const argsShort = parseArgs(
|
||||
initArgs(['https://google.com', `-${shortArg}`, value]),
|
||||
);
|
||||
) as Record<string, string>;
|
||||
expect(argsShort[arg]).toBe(value);
|
||||
|
||||
initArgs(['https://google.com', `-${shortArg}`, badValue]);
|
||||
@ -192,64 +196,74 @@ describe('initArgs + parseArgs', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ arg: 'always-on-top', shortArg: null },
|
||||
{ arg: 'block-external-urls', shortArg: null },
|
||||
{ arg: 'bounce', shortArg: null },
|
||||
{ arg: 'clear-cache', shortArg: null },
|
||||
{ arg: 'always-on-top', shortArg: '' },
|
||||
{ arg: 'block-external-urls', shortArg: '' },
|
||||
{ arg: 'bounce', shortArg: '' },
|
||||
{ arg: 'clear-cache', shortArg: '' },
|
||||
{ arg: 'conceal', shortArg: 'c' },
|
||||
{ arg: 'counter', shortArg: null },
|
||||
{ arg: 'darwin-dark-mode-support', shortArg: null },
|
||||
{ arg: 'disable-context-menu', shortArg: null },
|
||||
{ arg: 'disable-dev-tools', shortArg: null },
|
||||
{ arg: 'disable-gpu', shortArg: null },
|
||||
{ arg: 'disable-old-build-warning-yesiknowitisinsecure', shortArg: null },
|
||||
{ arg: 'enable-es3-apis', shortArg: null },
|
||||
{ arg: 'counter', shortArg: '' },
|
||||
{ arg: 'darwin-dark-mode-support', shortArg: '' },
|
||||
{ arg: 'disable-context-menu', shortArg: '' },
|
||||
{ arg: 'disable-dev-tools', shortArg: '' },
|
||||
{ arg: 'disable-gpu', shortArg: '' },
|
||||
{ arg: 'disable-old-build-warning-yesiknowitisinsecure', shortArg: '' },
|
||||
{ arg: 'enable-es3-apis', shortArg: '' },
|
||||
{ arg: 'fast-quit', shortArg: 'f' },
|
||||
{ arg: 'flash', shortArg: null },
|
||||
{ arg: 'full-screen', shortArg: null },
|
||||
{ arg: 'hide-window-frame', shortArg: null },
|
||||
{ arg: 'honest', shortArg: null },
|
||||
{ arg: 'ignore-certificate', shortArg: null },
|
||||
{ arg: 'ignore-gpu-blacklist', shortArg: null },
|
||||
{ arg: 'insecure', shortArg: null },
|
||||
{ arg: 'maximize', shortArg: null },
|
||||
{ arg: 'portable', shortArg: null },
|
||||
{ arg: 'flash', shortArg: '' },
|
||||
{ arg: 'full-screen', shortArg: '' },
|
||||
{ arg: 'hide-window-frame', shortArg: '' },
|
||||
{ arg: 'honest', shortArg: '' },
|
||||
{ arg: 'ignore-certificate', shortArg: '' },
|
||||
{ arg: 'ignore-gpu-blacklist', shortArg: '' },
|
||||
{ arg: 'insecure', shortArg: '' },
|
||||
{ arg: 'maximize', shortArg: '' },
|
||||
{ arg: 'portable', shortArg: '' },
|
||||
{ arg: 'show-menu-bar', shortArg: 'm' },
|
||||
{ arg: 'single-instance', shortArg: null },
|
||||
{ arg: 'tray', shortArg: null },
|
||||
{ arg: 'verbose', shortArg: null },
|
||||
{ arg: 'widevine', shortArg: null },
|
||||
{ arg: 'single-instance', shortArg: '' },
|
||||
{ arg: 'tray', shortArg: '' },
|
||||
{ arg: 'verbose', shortArg: '' },
|
||||
{ arg: 'widevine', shortArg: '' },
|
||||
])('test boolean arg %s', ({ arg, shortArg }) => {
|
||||
const defaultArgs = parseArgs(initArgs(['https://google.com']));
|
||||
const defaultArgs = parseArgs(initArgs(['https://google.com'])) as Record<
|
||||
string,
|
||||
boolean
|
||||
>;
|
||||
expect(defaultArgs[arg]).toBe(false);
|
||||
|
||||
const args = parseArgs(initArgs(['https://google.com', `--${arg}`]));
|
||||
const args = parseArgs(
|
||||
initArgs(['https://google.com', `--${arg}`]),
|
||||
) as Record<string, boolean>;
|
||||
expect(args[arg]).toBe(true);
|
||||
if (shortArg) {
|
||||
const argsShort = parseArgs(
|
||||
initArgs(['https://google.com', `-${shortArg}`]),
|
||||
);
|
||||
) as Record<string, boolean>;
|
||||
expect(argsShort[arg]).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([{ arg: 'no-overwrite', shortArg: null }])(
|
||||
test.each([{ arg: 'no-overwrite', shortArg: '' }])(
|
||||
'test inversible boolean arg %s',
|
||||
({ arg, shortArg }) => {
|
||||
const inverse = arg.startsWith('no-') ? arg.substr(3) : `no-${arg}`;
|
||||
|
||||
const defaultArgs = parseArgs(initArgs(['https://google.com']));
|
||||
const defaultArgs = parseArgs(initArgs(['https://google.com'])) as Record<
|
||||
string,
|
||||
boolean
|
||||
>;
|
||||
expect(defaultArgs[arg]).toBe(false);
|
||||
expect(defaultArgs[inverse]).toBe(true);
|
||||
|
||||
const args = parseArgs(initArgs(['https://google.com', `--${arg}`]));
|
||||
const args = parseArgs(
|
||||
initArgs(['https://google.com', `--${arg}`]),
|
||||
) as Record<string, boolean>;
|
||||
expect(args[arg]).toBe(true);
|
||||
expect(args[inverse]).toBe(false);
|
||||
|
||||
if (shortArg) {
|
||||
const argsShort = parseArgs(
|
||||
initArgs(['https://google.com', `-${shortArg as string}`]),
|
||||
);
|
||||
initArgs(['https://google.com', `-${shortArg}`]),
|
||||
) as Record<string, boolean>;
|
||||
expect(argsShort[arg]).toBe(true);
|
||||
expect(argsShort[inverse]).toBe(true);
|
||||
}
|
||||
@ -257,35 +271,35 @@ describe('initArgs + parseArgs', () => {
|
||||
);
|
||||
|
||||
test.each([
|
||||
{ arg: 'disk-cache-size', shortArg: null, value: 100 },
|
||||
{ arg: 'height', shortArg: null, value: 200 },
|
||||
{ arg: 'max-height', shortArg: null, value: 300 },
|
||||
{ arg: 'max-width', shortArg: null, value: 400 },
|
||||
{ arg: 'min-height', shortArg: null, value: 500 },
|
||||
{ arg: 'min-width', shortArg: null, value: 600 },
|
||||
{ arg: 'width', shortArg: null, value: 700 },
|
||||
{ arg: 'x', shortArg: null, value: 800 },
|
||||
{ arg: 'y', shortArg: null, value: 900 },
|
||||
{ arg: 'disk-cache-size', shortArg: '', value: 100 },
|
||||
{ arg: 'height', shortArg: '', value: 200 },
|
||||
{ arg: 'max-height', shortArg: '', value: 300 },
|
||||
{ arg: 'max-width', shortArg: '', value: 400 },
|
||||
{ arg: 'min-height', shortArg: '', value: 500 },
|
||||
{ arg: 'min-width', shortArg: '', value: 600 },
|
||||
{ arg: 'width', shortArg: '', value: 700 },
|
||||
{ arg: 'x', shortArg: '', value: 800 },
|
||||
{ arg: 'y', shortArg: '', value: 900 },
|
||||
])('test numeric arg %s', ({ arg, shortArg, value }) => {
|
||||
const args = parseArgs(
|
||||
initArgs(['https://google.com', `--${arg}`, `${value}`]),
|
||||
);
|
||||
) as Record<string, number>;
|
||||
expect(args[arg]).toBe(value);
|
||||
|
||||
const badArgs = parseArgs(
|
||||
initArgs(['https://google.com', `--${arg}`, 'abcd']),
|
||||
);
|
||||
) as Record<string, number>;
|
||||
expect(badArgs[arg]).toBeNaN();
|
||||
|
||||
if (shortArg) {
|
||||
const shortArgs = parseArgs(
|
||||
initArgs(['https://google.com', `-${shortArg as string}`, `${value}`]),
|
||||
);
|
||||
initArgs(['https://google.com', `-${shortArg}`, `${value}`]),
|
||||
) as Record<string, number>;
|
||||
expect(shortArgs[arg]).toBe(value);
|
||||
|
||||
const badShortArgs = parseArgs(
|
||||
initArgs(['https://google.com', `-${shortArg as string}`, 'abcd']),
|
||||
);
|
||||
initArgs(['https://google.com', `-${shortArg}`, 'abcd']),
|
||||
) as Record<string, number>;
|
||||
expect(badShortArgs[arg]).toBeNaN();
|
||||
}
|
||||
});
|
||||
|
36
src/cli.ts
36
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<any> {
|
||||
export function initArgs(argv: string[]): yargs.Argv<RawOptions> {
|
||||
const args = yargs(argv)
|
||||
.scriptName('nativefier')
|
||||
.usage(
|
||||
@ -160,7 +161,6 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
|
||||
coerce: parseJson,
|
||||
description:
|
||||
'override Electron BrowserWindow options (via JSON string); see https://github.com/nativefier/nativefier/blob/master/API.md#browserwindow-options',
|
||||
type: 'string',
|
||||
})
|
||||
.option('disable-context-menu', {
|
||||
default: false,
|
||||
@ -222,7 +222,6 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
|
||||
coerce: getProcessEnvs,
|
||||
description:
|
||||
'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened',
|
||||
type: 'string',
|
||||
})
|
||||
.option('single-instance', {
|
||||
default: false,
|
||||
@ -285,11 +284,11 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
|
||||
coerce: parseJson,
|
||||
description:
|
||||
'a JSON string defining file download options; see https://github.com/sindresorhus/electron-dl',
|
||||
type: 'string',
|
||||
})
|
||||
.option('inject', {
|
||||
description:
|
||||
'path to a CSS/JS file to be injected; pass multiple times to inject multiple files',
|
||||
string: true,
|
||||
type: 'array',
|
||||
})
|
||||
.option('lang', {
|
||||
@ -477,10 +476,10 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
|
||||
type: 'string',
|
||||
})
|
||||
.option('win32metadata', {
|
||||
coerce: parseJson,
|
||||
coerce: (value: string) =>
|
||||
parseJson<electronPackager.Win32MetadataOptions>(value),
|
||||
description:
|
||||
'(windows only) a JSON string of key/value pairs (ProductName, InternalName, FileDescription) to embed as executable metadata',
|
||||
type: 'string',
|
||||
})
|
||||
.group(
|
||||
[
|
||||
@ -526,17 +525,17 @@ function decorateYargOptionGroup(value: string): string {
|
||||
return `====== ${value} ======`;
|
||||
}
|
||||
|
||||
export function parseArgs(args: yargs.Argv<any>): any {
|
||||
export function parseArgs(args: yargs.Argv<RawOptions>): RawOptions {
|
||||
const parsed = { ...args.argv };
|
||||
// In yargs, the _ property of the parsed args is an array of the positional args
|
||||
// https://github.com/yargs/yargs/blob/master/docs/examples.md#and-non-hyphenated-options-too-just-use-argv_
|
||||
// So try to extract the targetUrl and outputDirectory from these
|
||||
parsed.targetUrl = parsed._.length > 0 ? parsed._[0].toString() : '';
|
||||
parsed.out = parsed._.length > 1 ? parsed._[1] : '';
|
||||
parsed.targetUrl = parsed._.length > 0 ? parsed._[0].toString() : undefined;
|
||||
parsed.out = parsed._.length > 1 ? (parsed._[1] as string) : undefined;
|
||||
|
||||
if (parsed.upgrade && parsed.targetUrl !== '') {
|
||||
if (parsed.upgrade && parsed.targetUrl) {
|
||||
let targetAndUpgrade = false;
|
||||
if (parsed.out === '') {
|
||||
if (!parsed.out) {
|
||||
// If we're upgrading, the first positional args might be the outputDirectory, so swap these if we can
|
||||
try {
|
||||
// If this succeeds, we have a problem
|
||||
@ -545,7 +544,7 @@ export function parseArgs(args: yargs.Argv<any>): any {
|
||||
} catch {
|
||||
// Cool, it's not a URL
|
||||
parsed.out = parsed.targetUrl;
|
||||
parsed.targetUrl = '';
|
||||
parsed.targetUrl = undefined;
|
||||
}
|
||||
} else {
|
||||
// Someone supplied a targetUrl, an outputDirectory, and --upgrade. That's not cool.
|
||||
@ -559,7 +558,7 @@ export function parseArgs(args: yargs.Argv<any>): any {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.targetUrl === '' && !parsed.upgrade) {
|
||||
if (!parsed.targetUrl && !parsed.upgrade) {
|
||||
throw new Error(
|
||||
'ERROR: Nativefier must be called with either a targetUrl or the --upgrade option.\n',
|
||||
);
|
||||
@ -572,7 +571,7 @@ export function parseArgs(args: yargs.Argv<any>): any {
|
||||
|
||||
if (require.main === module) {
|
||||
// Not sure if we still need this with yargs. Keeping for now.
|
||||
const sanitizedArgs = [];
|
||||
const sanitizedArgs: string[] = [];
|
||||
process.argv.forEach((arg) => {
|
||||
if (isArgFormatInvalid(arg)) {
|
||||
throw new Error(
|
||||
@ -593,11 +592,12 @@ if (require.main === module) {
|
||||
sanitizedArgs.push(arg);
|
||||
});
|
||||
|
||||
let args, parsedArgs;
|
||||
let args: yargs.Argv<RawOptions> | undefined = undefined;
|
||||
let parsedArgs: RawOptions;
|
||||
try {
|
||||
args = initArgs(sanitizedArgs.slice(2));
|
||||
parsedArgs = parseArgs(args);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (args) {
|
||||
log.error(err);
|
||||
args.showHelp();
|
||||
@ -607,7 +607,7 @@ if (require.main === module) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const options: NativefierOptions = {
|
||||
const options: RawOptions = {
|
||||
...parsedArgs,
|
||||
};
|
||||
|
||||
|
@ -17,11 +17,17 @@ tmp.setGracefulCleanup(); // cleanup temp dirs even when an uncaught exception o
|
||||
const now = new Date();
|
||||
const TMP_TIME = `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`;
|
||||
|
||||
type DownloadResult = {
|
||||
export type DownloadResult = {
|
||||
data: Buffer;
|
||||
ext: string;
|
||||
};
|
||||
|
||||
type ProcessEnvs = Record<string, unknown>;
|
||||
|
||||
export function hasWine(): boolean {
|
||||
return hasbin.sync('wine');
|
||||
}
|
||||
|
||||
export function isOSX(): boolean {
|
||||
return os.platform() === 'darwin';
|
||||
}
|
||||
@ -57,7 +63,7 @@ export async function copyFileOrDir(
|
||||
dest: string,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ncp(sourceFileOrDir, dest, (error: any) => {
|
||||
ncp(sourceFileOrDir, dest, (error: Error[] | null): void => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
@ -66,15 +72,17 @@ export async function copyFileOrDir(
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadFile(fileUrl: string): Promise<DownloadResult> {
|
||||
export function downloadFile(
|
||||
fileUrl: string,
|
||||
): Promise<DownloadResult | undefined> {
|
||||
log.debug(`Downloading ${fileUrl}`);
|
||||
return axios
|
||||
.get(fileUrl, {
|
||||
.get<Buffer>(fileUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.data) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
data: response.data,
|
||||
@ -97,7 +105,7 @@ export function getAllowedIconFormats(platform: string): string[] {
|
||||
const icnsToPng = false;
|
||||
const icnsToIco = false;
|
||||
|
||||
const formats = [];
|
||||
const formats: string[] = [];
|
||||
|
||||
// Shell scripting is not supported on windows, temporary override
|
||||
if (isWindows()) {
|
||||
@ -175,11 +183,11 @@ export function generateRandomSuffix(length = 6): string {
|
||||
return hash.digest('hex').substring(0, length);
|
||||
}
|
||||
|
||||
export function getProcessEnvs(val: string): any {
|
||||
export function getProcessEnvs(val: string): ProcessEnvs | undefined {
|
||||
if (!val) {
|
||||
return {};
|
||||
return undefined;
|
||||
}
|
||||
return parseJson(val);
|
||||
return parseJson<ProcessEnvs>(val);
|
||||
}
|
||||
|
||||
export function checkInternet(): void {
|
||||
|
@ -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}`);
|
||||
|
@ -8,7 +8,7 @@ import { spawnSync } from 'child_process';
|
||||
export function getVersionString(
|
||||
executablePath: string,
|
||||
versionString: string,
|
||||
): string {
|
||||
): string | undefined {
|
||||
let rcedit = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
|
@ -3,10 +3,11 @@ import * as path from 'path';
|
||||
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { NativefierOptions } from '../../options/model';
|
||||
import { NativefierOptions, RawOptions } from '../../options/model';
|
||||
import { dirExists, fileExists } from '../fsHelpers';
|
||||
import { extractBoolean, extractString } from './plistInfoXMLHelpers';
|
||||
import { getOptionsFromExecutable } from './executableHelpers';
|
||||
import { parseJson } from '../../utils/parseUtils';
|
||||
|
||||
export type UpgradeAppInfo = {
|
||||
appResourcesDir: string;
|
||||
@ -79,7 +80,9 @@ function getInfoPListOptions(
|
||||
'NSHumanReadableCopyright',
|
||||
);
|
||||
log.debug(
|
||||
`Extracted app copyright from Info.plist: ${newOptions.appCopyright}`,
|
||||
`Extracted app copyright from Info.plist: ${
|
||||
newOptions.appCopyright as string
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -96,7 +99,9 @@ function getInfoPListOptions(
|
||||
? undefined
|
||||
: newOptions.darwinDarkModeSupport === false),
|
||||
log.debug(
|
||||
`Extracted app version from Info.plist: ${newOptions.appVersion}`,
|
||||
`Extracted app version from Info.plist: ${
|
||||
newOptions.appVersion as string
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -152,11 +157,19 @@ export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug(`Loading ${path.join(appResourcesDir, 'nativefier.json')}`);
|
||||
const options: NativefierOptions = JSON.parse(
|
||||
fs.readFileSync(path.join(appResourcesDir, 'nativefier.json'), 'utf8'),
|
||||
const nativefierJSONPath = path.join(appResourcesDir, 'nativefier.json');
|
||||
|
||||
log.debug(`Loading ${nativefierJSONPath}`);
|
||||
const options = parseJson<NativefierOptions>(
|
||||
fs.readFileSync(nativefierJSONPath, 'utf8'),
|
||||
);
|
||||
|
||||
if (!options) {
|
||||
throw new Error(
|
||||
`Could not read Nativefier options from ${nativefierJSONPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
options.electronVersion = undefined;
|
||||
|
||||
return {
|
||||
@ -173,9 +186,9 @@ export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null {
|
||||
}
|
||||
|
||||
export function useOldAppOptions(
|
||||
rawOptions: NativefierOptions,
|
||||
rawOptions: RawOptions,
|
||||
oldApp: UpgradeAppInfo,
|
||||
): NativefierOptions {
|
||||
): RawOptions {
|
||||
if (rawOptions.targetUrl !== undefined && dirExists(rawOptions.targetUrl)) {
|
||||
// You got your ouput dir in my targetUrl!
|
||||
rawOptions.out = rawOptions.targetUrl;
|
||||
|
@ -31,7 +31,7 @@ export async function getChromeVersionForElectronVersion(
|
||||
|
||||
try {
|
||||
log.debug('Grabbing electron<->chrome versions file from', url);
|
||||
const response = await axios.get(url, { timeout: 5000 });
|
||||
const response = await axios.get<ElectronRelease[]>(url, { timeout: 5000 });
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Bad request: Status code ${response.status}`);
|
||||
}
|
||||
@ -50,7 +50,7 @@ export async function getChromeVersionForElectronVersion(
|
||||
`Associated electron v${electronVersion} to chrome v${chromeVersion}`,
|
||||
);
|
||||
return chromeVersion;
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
log.error('getChromeVersionForElectronVersion ERROR', err);
|
||||
log.debug('Falling back to default Chrome version', DEFAULT_CHROME_VERSION);
|
||||
return DEFAULT_CHROME_VERSION;
|
||||
|
@ -28,7 +28,7 @@ export async function getLatestFirefoxVersion(
|
||||
): Promise<string> {
|
||||
try {
|
||||
log.debug('Grabbing Firefox version data from', url);
|
||||
const response = await axios.get(url, { timeout: 5000 });
|
||||
const response = await axios.get<FirefoxVersions>(url, { timeout: 5000 });
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Bad request: Status code ${response.status}`);
|
||||
}
|
||||
@ -38,7 +38,7 @@ export async function getLatestFirefoxVersion(
|
||||
`Got latest Firefox version ${firefoxVersions.LATEST_FIREFOX_VERSION}`,
|
||||
);
|
||||
return firefoxVersions.LATEST_FIREFOX_VERSION;
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
log.error('getLatestFirefoxVersion ERROR', err);
|
||||
log.debug(
|
||||
'Falling back to default Firefox version',
|
||||
|
@ -16,7 +16,7 @@ export async function getLatestSafariVersion(
|
||||
): Promise<SafariVersion> {
|
||||
try {
|
||||
log.debug('Grabbing apple version data from', url);
|
||||
const response = await axios.get(url, { timeout: 5000 });
|
||||
const response = await axios.get<string>(url, { timeout: 5000 });
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Bad request: Status code ${response.status}`);
|
||||
}
|
||||
@ -39,8 +39,8 @@ export async function getLatestSafariVersion(
|
||||
|
||||
const versionRows = majorVersionTable.split('<tbody')[1].split('<tr');
|
||||
|
||||
let version = null;
|
||||
let webkitVersion: string = null;
|
||||
let version: string | undefined = undefined;
|
||||
let webkitVersion: string | undefined = undefined;
|
||||
|
||||
for (const versionRow of versionRows.reverse()) {
|
||||
const versionMatch = [
|
||||
@ -61,12 +61,15 @@ export async function getLatestSafariVersion(
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
majorVersion,
|
||||
version,
|
||||
webkitVersion,
|
||||
};
|
||||
} catch (err) {
|
||||
if (version && webkitVersion) {
|
||||
return {
|
||||
majorVersion,
|
||||
version,
|
||||
webkitVersion,
|
||||
};
|
||||
}
|
||||
return DEFAULT_SAFARI_VERSION;
|
||||
} catch (err: unknown) {
|
||||
log.error('getLatestSafariVersion ERROR', err);
|
||||
log.debug('Falling back to default Safari version', DEFAULT_SAFARI_VERSION);
|
||||
return DEFAULT_SAFARI_VERSION;
|
||||
|
@ -7,6 +7,7 @@ import * as pageIcon from 'page-icon';
|
||||
|
||||
import {
|
||||
downloadFile,
|
||||
DownloadResult,
|
||||
getAllowedIconFormats,
|
||||
getTempDir,
|
||||
} from '../helpers/helpers';
|
||||
@ -17,10 +18,17 @@ const writeFileAsync = promisify(writeFile);
|
||||
const GITCLOUD_SPACE_DELIMITER = '-';
|
||||
const GITCLOUD_URL = 'https://nativefier.github.io/nativefier-icons/';
|
||||
|
||||
function getMaxMatchScore(iconWithScores: any[]): number {
|
||||
type GitCloudIcon = {
|
||||
ext?: string;
|
||||
name?: string;
|
||||
score?: number;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
function getMaxMatchScore(iconWithScores: GitCloudIcon[]): number {
|
||||
const score = iconWithScores.reduce((maxScore, currentIcon) => {
|
||||
const currentScore = currentIcon.score;
|
||||
if (currentScore > maxScore) {
|
||||
if (currentScore && currentScore > maxScore) {
|
||||
return currentScore;
|
||||
}
|
||||
return maxScore;
|
||||
@ -29,36 +37,42 @@ function getMaxMatchScore(iconWithScores: any[]): number {
|
||||
return score;
|
||||
}
|
||||
|
||||
function getMatchingIcons(iconsWithScores: any[], maxScore: number): any[] {
|
||||
return iconsWithScores
|
||||
.filter((item) => item.score === maxScore)
|
||||
.map((item) => ({ ...item, ext: path.extname(item.url) }));
|
||||
function getMatchingIcons(
|
||||
iconsWithScores: GitCloudIcon[],
|
||||
maxScore: number,
|
||||
): GitCloudIcon[] {
|
||||
return iconsWithScores.filter((item) => item.score === maxScore);
|
||||
}
|
||||
|
||||
function mapIconWithMatchScore(
|
||||
cloudIcons: { name: string; url: string }[],
|
||||
targetUrl: string,
|
||||
): any {
|
||||
): GitCloudIcon[] {
|
||||
const normalisedTargetUrl = targetUrl.toLowerCase();
|
||||
return cloudIcons.map((item) => {
|
||||
const itemWords = item.name.split(GITCLOUD_SPACE_DELIMITER);
|
||||
const score = itemWords.reduce((currentScore: number, word: string) => {
|
||||
if (normalisedTargetUrl.includes(word)) {
|
||||
return currentScore + 1;
|
||||
}
|
||||
return currentScore;
|
||||
}, 0);
|
||||
const score: number = itemWords.reduce(
|
||||
(currentScore: number, word: string) => {
|
||||
if (normalisedTargetUrl.includes(word)) {
|
||||
return currentScore + 1;
|
||||
}
|
||||
return currentScore;
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
return { ...item, score };
|
||||
return { ...item, ext: path.extname(item.url), score };
|
||||
});
|
||||
}
|
||||
|
||||
async function inferIconFromStore(
|
||||
targetUrl: string,
|
||||
platform: string,
|
||||
): Promise<any> {
|
||||
): Promise<DownloadResult | undefined> {
|
||||
log.debug(`Inferring icon from store for ${targetUrl} on ${platform}`);
|
||||
const allowedFormats = new Set(getAllowedIconFormats(platform));
|
||||
const allowedFormats = new Set<string | undefined>(
|
||||
getAllowedIconFormats(platform),
|
||||
);
|
||||
|
||||
const cloudIcons = await gitCloud(GITCLOUD_URL);
|
||||
log.debug(`Got ${cloudIcons.length} icons from gitcloud`);
|
||||
@ -67,19 +81,19 @@ async function inferIconFromStore(
|
||||
|
||||
if (maxScore === 0) {
|
||||
log.debug('No relevant icon in store.');
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iconsMatchingScore = getMatchingIcons(iconWithScores, maxScore);
|
||||
const iconsMatchingExt = iconsMatchingScore.filter((icon) =>
|
||||
allowedFormats.has(icon.ext),
|
||||
allowedFormats.has(icon.ext ?? path.extname(icon.url as string)),
|
||||
);
|
||||
const matchingIcon = iconsMatchingExt[0];
|
||||
const iconUrl = matchingIcon && matchingIcon.url;
|
||||
|
||||
if (!iconUrl) {
|
||||
log.debug('Could not infer icon from store');
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
return downloadFile(iconUrl);
|
||||
}
|
||||
@ -87,21 +101,19 @@ async function inferIconFromStore(
|
||||
export async function inferIcon(
|
||||
targetUrl: string,
|
||||
platform: string,
|
||||
): Promise<string> {
|
||||
): Promise<string | undefined> {
|
||||
log.debug(`Inferring icon for ${targetUrl} on ${platform}`);
|
||||
const tmpDirPath = getTempDir('iconinfer');
|
||||
|
||||
let icon: { ext: string; data: Buffer } = await inferIconFromStore(
|
||||
targetUrl,
|
||||
platform,
|
||||
);
|
||||
let icon: { ext: string; data: Buffer } | undefined =
|
||||
await inferIconFromStore(targetUrl, platform);
|
||||
if (!icon) {
|
||||
const ext = platform === 'win32' ? '.ico' : '.png';
|
||||
log.debug(`Trying to extract a ${ext} icon from the page.`);
|
||||
icon = await pageIcon(targetUrl, { ext });
|
||||
}
|
||||
if (!icon) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
log.debug(`Got an icon from the page.`);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ const USER_AGENT =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36';
|
||||
|
||||
export async function inferTitle(url: string): Promise<string> {
|
||||
const { data } = await axios.get(url, {
|
||||
const { data } = await axios.get<string>(url, {
|
||||
headers: {
|
||||
// Fake user agent for pages like http://messenger.com
|
||||
'User-Agent': USER_AGENT,
|
||||
@ -13,7 +13,7 @@ export async function inferTitle(url: string): Promise<string> {
|
||||
});
|
||||
log.debug(`Fetched ${(data.length / 1024).toFixed(1)} kb page at`, url);
|
||||
const inferredTitle =
|
||||
/<\s*title.*?>(?<title>.+?)<\s*\/title\s*?>/i.exec(data).groups?.title ||
|
||||
/<\s*title.*?>(?<title>.+?)<\s*\/title\s*?>/i.exec(data)?.groups?.title ??
|
||||
'Webapp';
|
||||
log.debug('Inferred title:', inferredTitle);
|
||||
return inferredTitle;
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
@ -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\\"}"`
|
||||
: '';
|
||||
|
@ -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);
|
||||
|
@ -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/**/*"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user