mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2024-12-22 18:18:55 +00:00
Revamp and move to TypeScript (#898)
## Breaking changes - Require **Node >= 8.10.0 and npm 5.6.0** - Move to **Electron 8.1.1**. - That's it. Lots of care went into breaking CLI & programmatic behavior as little as possible. **Please report regressions**. - Known issue: build may fail behind a proxy. Get in touch if you use one: https://github.com/jiahaog/nativefier/issues/907#issuecomment-596144768 ## Changes summary Nativefier didn't get much love recently, to the point that it's becoming hard to run on recent Node, due to old dependencies. Also, some past practices now seem weird, as better expressible by modern JS/TS, discouraging contributions including mine. Addressing this, and one thing leading to another, came a bigger-than-expected revamp, aiming at making Nativefier more **lean, stable, future-proof, user-friendly and dev-friendly**, while **not changing the CLI/programmatic interfaces**. Highlights: - **Require Node>=8**, as imposed by many of our dependencies. Node 8 is twice LTS, and easily available even in conservative Linux distros. No reason not to demand it. - **Default to Electron 8**. - **Bump** all dependencies to latest version, including electron-packager. - **Move to TS**. TS is great. As of today, I see no reason not to use it, and fight interface bugs at runtime rather than at compile time. With that, get rid of everything Babel/Webpack. - **Move away from Gulp**. Gulp's selling point is perf via streaming, but for small builds like Nativefier, npm tasks are plenty good and less dependency bloat. Gulp was the driver for this PR: broken on Node 12, and I didn't feel like just upgrading and keeping it. - Add tons of **verbose logs** everywhere it makes sense, to have a fine & clear trace of the program flow. This will be helpful to debug user-reported issues, and already helped me fix a few bugs. - With better simple logging, get rid of the quirky and buggy progress bar based on package `progress`. Nice logging (minimal by default, the verbose logging mentioned above is only used when passing `--verbose`) is better and one less dependency. - **Dump `async` package**, a relic from old callback-hell early Node. Also dump a few other micro-packages unnecessary now. - A first pass of code **cleanup** thanks to modern JS/TS features: fixes, simplifications, jsdoc type annotations to types, etc. - **Remove GitHub integrations Hound & CodeClimate**, which are more exotic than good'ol'linters, and whose signal-to-noise ratio is too low. - Quality: **Add tests** and add **Windows + macOS CI builds**. Also, add a **manual test script**, helping to quickly verify the hard-to-programatically-test stuff before releases, and limit regressions. - **Fix a very small number of existing bugs**. The goal of this PR was *not* to fix bugs, but to get Nativefier in better shape to do so. Bugfixes will come later. Still, these got addressed: - Add common `Alt`+`Left`/`Right` for previous/next navigation. - Improve #379: fix zoom with `Ctrl` + numpad `+`/`-` - Fix pinch-to-zoom (see https://github.com/jiahaog/nativefier/issues/379#issuecomment-598612128 )
This commit is contained in:
parent
f115beed0d
commit
c9ee6667d4
@ -1,18 +0,0 @@
|
||||
---
|
||||
engines:
|
||||
csslint:
|
||||
enabled: true
|
||||
duplication:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
- javascript
|
||||
eslint:
|
||||
enabled: false
|
||||
fixme:
|
||||
enabled: true
|
||||
ratings:
|
||||
paths:
|
||||
- "**.js"
|
||||
exclude_paths:
|
||||
- test/
|
23
.eslintrc.js
Normal file
23
.eslintrc.js
Normal file
@ -0,0 +1,23 @@
|
||||
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'prettier'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'prettier',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
// TODO remove when done killing anys and making tsc strict
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
},
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
extends:
|
||||
- airbnb-base
|
||||
- prettier
|
||||
env:
|
||||
# TODO: find out how to turn this on only for src/**/*.test.js files
|
||||
jest: true
|
||||
plugins:
|
||||
- import
|
||||
- prettier
|
||||
rules:
|
||||
# TODO: Remove this when we have shifted away from the async package
|
||||
no-shadow: 'warn'
|
||||
# Gulpfiles and tests use dev dependencies
|
||||
import/no-extraneous-dependencies: ['error', { devDependencies: ['gulpfile.babel.js', 'gulp/**/**.js', 'test/**/**.js']}]
|
||||
prettier/prettier: "error"
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -5,7 +5,7 @@
|
||||
package-lock.json
|
||||
|
||||
# ignore compiled lib files
|
||||
lib/*
|
||||
lib*
|
||||
app/lib/*
|
||||
built-tests
|
||||
|
||||
@ -48,3 +48,6 @@ node_modules
|
||||
*.iml
|
||||
out
|
||||
gen
|
||||
|
||||
# Builds when testing npm pack
|
||||
nativefier*.tgz
|
||||
|
@ -1,7 +0,0 @@
|
||||
eslint:
|
||||
enabled: true
|
||||
config_file: .eslintrc.yml
|
||||
ignore_file: .eslintignore
|
||||
|
||||
jshint:
|
||||
enabled: false
|
20
.npmignore
20
.npmignore
@ -1,7 +1,19 @@
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
/*
|
||||
!lib/
|
||||
!app/lib
|
||||
!bin
|
||||
!app/lib/
|
||||
!icon-scripts
|
||||
.DS_Store
|
||||
.eslintrc.yml
|
||||
src/
|
||||
app/src/
|
||||
app/node_modules
|
||||
*tsconfig.tsbuildinfo
|
||||
*package-lock.json
|
||||
*tsconfig.json
|
||||
*jestSetupFiles*
|
||||
*-test.js
|
||||
*-test.js.map
|
||||
*.test.d.ts
|
||||
*.test.js
|
||||
*.test.js.map
|
||||
|
26
.travis.yml
26
.travis.yml
@ -1,21 +1,19 @@
|
||||
language: node_js
|
||||
addons:
|
||||
code_climate:
|
||||
repo_token: CODE_CLIMATE_TOKEN
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
node_js:
|
||||
- '11'
|
||||
- '10'
|
||||
- '13' # Changing this? Remind to adjust the linter condition below causing linter to run for only one version (for faster CI)
|
||||
- '12'
|
||||
- '8'
|
||||
- '7'
|
||||
- '6'
|
||||
before_install:
|
||||
- npm install -g npm@5.8.x
|
||||
install:
|
||||
- npm run dev-up
|
||||
- npm install
|
||||
- npm run build
|
||||
script:
|
||||
- npm run ci
|
||||
after_script:
|
||||
- codeclimate-test-reporter < ./coverage/lcov.info
|
||||
# Only run linter once, for faster CI
|
||||
- if [ "$TRAVIS_OS_NAME" = "linux" ] && [ "$TRAVIS_NODE_VERSION" = "13" ]; then npm run lint; fi
|
||||
- npm test
|
||||
deploy:
|
||||
provider: npm
|
||||
skip_cleanup: true
|
||||
@ -25,4 +23,4 @@ deploy:
|
||||
on:
|
||||
tags: true
|
||||
repo: jiahaog/nativefier
|
||||
node: '8'
|
||||
node: '12'
|
||||
|
59
README.md
59
README.md
@ -1,11 +1,9 @@
|
||||
# Nativefier
|
||||
|
||||
[![Build Status](https://travis-ci.org/jiahaog/nativefier.svg?branch=development)](https://travis-ci.org/jiahaog/nativefier)
|
||||
[![Code Climate](https://codeclimate.com/github/jiahaog/nativefier/badges/gpa.svg)](https://codeclimate.com/github/jiahaog/nativefier)
|
||||
[![Build Status](https://travis-ci.org/jiahaog/nativefier.svg)](https://travis-ci.org/jiahaog/nativefier)
|
||||
[![npm version](https://badge.fury.io/js/nativefier.svg)](https://www.npmjs.com/package/nativefier)
|
||||
[![Dependency Status](https://david-dm.org/jiahaog/nativefier.svg)](https://david-dm.org/jiahaog/nativefier)
|
||||
|
||||
![Dock](screenshots/dock.png)
|
||||
![Dock](dock.png)
|
||||
|
||||
You want to make a native wrapper for WhatsApp Web (or any web page).
|
||||
|
||||
@ -13,7 +11,7 @@ You want to make a native wrapper for WhatsApp Web (or any web page).
|
||||
nativefier web.whatsapp.com
|
||||
```
|
||||
|
||||
![Walkthrough](screenshots/walkthrough.gif)
|
||||
![Walkthrough animation](walkthrough.gif)
|
||||
|
||||
You're done.
|
||||
|
||||
@ -21,33 +19,31 @@ You're done.
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Optional dependencies](#optional-dependencies)
|
||||
- [How it works](#how-it-works)
|
||||
- [Development](docs/development.md)
|
||||
- [License](#license)
|
||||
|
||||
## Introduction
|
||||
|
||||
Nativefier is a command-line tool to easily create a desktop application for any web site with succinct and minimal configuration. Apps are wrapped by [Electron](http://electron.atom.io) in an OS executable (`.app`, `.exe`, etc.) for use on Windows, macOS and Linux.
|
||||
Nativefier is a command-line tool to easily create a desktop application for any web site with succinct and minimal configuration. Apps are wrapped by [Electron](https://www.electronjs.org/) in an OS executable (`.app`, `.exe`, etc.) for use on Windows, macOS and Linux.
|
||||
|
||||
I did this because I was tired of having to `⌘-tab` or `alt-tab` to my browser and then search through the numerous open tabs when I was using [Facebook Messenger](http://messenger.com) or [Whatsapp Web](http://web.whatsapp.com) ([relevant Hacker News thread](https://news.ycombinator.com/item?id=10930718)).
|
||||
I did this because I was tired of having to `⌘-tab` or `alt-tab` to my browser and then search through the numerous open tabs when I was using [Facebook Messenger](https://messenger.com) or [Whatsapp Web](https://web.whatsapp.com) ([relevant Hacker News thread](https://news.ycombinator.com/item?id=10930718)).
|
||||
|
||||
[Changelog](https://github.com/jiahaog/nativefier/blob/master/docs/changelog.md). [Developer docs](https://github.com/jiahaog/nativefier/blob/master/docs/development.md).
|
||||
[Changelog](https://github.com/jiahaog/nativefier/blob/master/CHANGELOG.md). [Developer docs](https://github.com/jiahaog/nativefier/blob/master/docs/development.md).
|
||||
|
||||
### Features
|
||||
Features:
|
||||
|
||||
- Automatically retrieves the correct icon and app name.
|
||||
- JavaScript and CSS injection.
|
||||
- Flash Support (with [`--flash`](docs/api.md#flash) flag).
|
||||
- Many more, see the [API docs](docs/api.md) or `nativefier --help`
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
- macOS 10.9+ / Windows / Linux
|
||||
- [Node.js](https://nodejs.org/) `>=6` (4.x may work but is no longer tested, please upgrade)
|
||||
- See [optional dependencies](#optional-dependencies) for more.
|
||||
- [Node.js](https://nodejs.org/) `>=8`
|
||||
- Optional dependencies:
|
||||
- [ImageMagick](http://www.imagemagick.org/) to convert icons. Make sure `convert` and `identify` are in your `$PATH`.
|
||||
- [Wine](https://www.winehq.org/) to package Windows apps under non-Windows platforms. Make sure `wine` is in your `$PATH`.
|
||||
|
||||
```bash
|
||||
npm install nativefier -g
|
||||
@ -55,42 +51,23 @@ npm install nativefier -g
|
||||
|
||||
## Usage
|
||||
|
||||
Creating a native desktop app for [medium.com](http://medium.com):
|
||||
Creating a native desktop app for [medium.com](https://medium.com):
|
||||
|
||||
```bash
|
||||
nativefier "http://medium.com"
|
||||
nativefier "medium.com"
|
||||
```
|
||||
|
||||
Nativefier will intelligently attempt to determine the app name, your OS and processor architecture, among other options. If desired, the app name or other options can be overwritten by specifying the `--name "Medium"` as part of the command line options:
|
||||
Nativefier will attempt to determine the app name, your OS and processor architecture, among other options. If desired, the app name or other options can be overwritten by specifying the `--name "Medium"` as part of the command line options:
|
||||
|
||||
```bash
|
||||
nativefier --name "Some Awesome App" "http://medium.com"
|
||||
nativefier --name "Some Awesome App" "medium.com"
|
||||
```
|
||||
Read the [API documentation](docs/api.md) (or `nativefier --help`) for other command line flags and options that can be used to configure the packaged app.
|
||||
|
||||
If you would like high resolution icons to be used, please contribute to the [icon repository](https://github.com/jiahaog/nativefier-icons)!
|
||||
Read the [API documentation](docs/api.md) (or `nativefier --help`) for other command-line flags that can be used to configure the packaged app.
|
||||
|
||||
**Windows Users:** Take note that the application menu is automatically hidden by default, you can press `alt` on your keyboard to access it.
|
||||
To have high-resolution icons used by default for an app/domain, please contribute to the [icon repository](https://github.com/jiahaog/nativefier-icons)!
|
||||
|
||||
**Linux Users:** Do not put spaces if you define the app name yourself with `--name`, as this will cause problems when pinning a packaged app to the launcher.
|
||||
|
||||
## Optional dependencies
|
||||
|
||||
### Icons for Windows apps packaged under non-Windows platforms
|
||||
|
||||
You need [Wine](https://www.winehq.org/) installed; make sure that `wine` is in your `$PATH`.
|
||||
|
||||
### Icon conversion for macOS
|
||||
|
||||
To support conversion of a `.png` or `.ico` into a `.icns` for a packaged macOS app icon (currently only supported on macOS), you need the following dependencies.
|
||||
|
||||
* [iconutil](https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html) (comes with [Xcode](https://developer.apple.com/xcode/)).
|
||||
* [imagemagick](http://www.imagemagick.org/script/index.php). Make sure `convert` and `identify` are in your `$PATH`.
|
||||
* If the tools are not found, then Nativefier will fall back to the built-in macOS tool `sips` to perform the conversion, which is more limited.
|
||||
|
||||
### Flash
|
||||
|
||||
[Google Chrome](https://www.google.com/chrome/) is required for flash to be supported; you should pass the path to its embedded Flash plugin to the `--flash` flag. See the [API docs](docs/api.md) for more details.
|
||||
Note that the application menu is hidden by default for a minimal UI. You can press the `alt` keyboard key to access it.
|
||||
|
||||
## How it works
|
||||
|
||||
|
24
app/.eslintrc.js
Normal file
24
app/.eslintrc.js
Normal file
@ -0,0 +1,24 @@
|
||||
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'prettier',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
// TODO remove when done killing anys and making tsc strict
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
},
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
settings:
|
||||
import/core-modules: [ electron ]
|
@ -3,23 +3,24 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Placeholder for the nativefier cli to override with a target url",
|
||||
"main": "lib/main.js",
|
||||
"dependencies": {
|
||||
"electron-context-menu": "^0.10.0",
|
||||
"electron-dl": "^1.10.0",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-window-state": "^4.1.1",
|
||||
"loglevel": "^1.5.1",
|
||||
"source-map-support": "^0.5.0",
|
||||
"wurl": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Jia Hao",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"desktop",
|
||||
"electron"
|
||||
"electron",
|
||||
"placeholder"
|
||||
],
|
||||
"author": "Jia Hao",
|
||||
"license": "MIT"
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"electron-context-menu": "0.x",
|
||||
"electron-dl": "3.x",
|
||||
"electron-squirrel-startup": "1.x",
|
||||
"electron-window-state": "5.x",
|
||||
"loglevel": "1.x",
|
||||
"source-map-support": "0.x",
|
||||
"wurl": "2.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "8.x"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { shell } from 'electron';
|
||||
import contextMenu from 'electron-context-menu';
|
||||
|
||||
function initContextMenu(createNewWindow, createNewTab) {
|
||||
export function initContextMenu(createNewWindow, createNewTab): void {
|
||||
contextMenu({
|
||||
prepend: (params) => {
|
||||
prepend: (actions, params) => {
|
||||
const items = [];
|
||||
if (params.linkURL) {
|
||||
items.push({
|
||||
@ -31,5 +31,3 @@ function initContextMenu(createNewWindow, createNewTab) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default initContextMenu;
|
@ -1,18 +1,19 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import path from 'path';
|
||||
import * as path from 'path';
|
||||
|
||||
function createLoginWindow(loginCallback) {
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
export function createLoginWindow(loginCallback): BrowserWindow {
|
||||
const loginWindow = new BrowserWindow({
|
||||
width: 300,
|
||||
height: 400,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegration: true, // TODO work around this; insecure
|
||||
},
|
||||
});
|
||||
loginWindow.loadURL(
|
||||
`file://${path.join(__dirname, '/static/login/login.html')}`,
|
||||
`file://${path.join(__dirname, '..', 'static/login.html')}`,
|
||||
);
|
||||
|
||||
ipcMain.once('login-message', (event, usernameAndPassword) => {
|
||||
@ -21,5 +22,3 @@ function createLoginWindow(loginCallback) {
|
||||
});
|
||||
return loginWindow;
|
||||
}
|
||||
|
||||
export default createLoginWindow;
|
@ -1,13 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { BrowserWindow, shell, ipcMain, dialog } from 'electron';
|
||||
import windowStateKeeper from 'electron-window-state';
|
||||
import mainWindowHelpers from './mainWindowHelpers';
|
||||
import helpers from '../../helpers/helpers';
|
||||
import createMenu from '../menu/menu';
|
||||
import initContextMenu from '../contextMenu/contextMenu';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const {
|
||||
import { BrowserWindow, shell, ipcMain, dialog, Event } from 'electron';
|
||||
import windowStateKeeper from 'electron-window-state';
|
||||
|
||||
import {
|
||||
isOSX,
|
||||
linkIsInternal,
|
||||
getCssToInject,
|
||||
@ -15,13 +12,19 @@ const {
|
||||
getAppIcon,
|
||||
nativeTabsSupported,
|
||||
getCounterValue,
|
||||
} = helpers;
|
||||
|
||||
const { onNewWindowHelper } = mainWindowHelpers;
|
||||
} from '../helpers/helpers';
|
||||
import { initContextMenu } from './contextMenu';
|
||||
import { onNewWindowHelper } from './mainWindowHelpers';
|
||||
import { createMenu } from './menu';
|
||||
|
||||
const ZOOM_INTERVAL = 0.1;
|
||||
|
||||
function maybeHideWindow(window, event, fastQuit, tray) {
|
||||
function hideWindow(
|
||||
window: BrowserWindow,
|
||||
event: Event,
|
||||
fastQuit: boolean,
|
||||
tray,
|
||||
): void {
|
||||
if (isOSX() && !fastQuit) {
|
||||
// this is called when exiting from clicking the cross button on the window
|
||||
event.preventDefault();
|
||||
@ -33,66 +36,51 @@ function maybeHideWindow(window, event, fastQuit, tray) {
|
||||
// will close the window on other platforms
|
||||
}
|
||||
|
||||
function maybeInjectCss(browserWindow) {
|
||||
function injectCss(browserWindow: BrowserWindow): void {
|
||||
if (!shouldInjectCss()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cssToInject = getCssToInject();
|
||||
|
||||
const injectCss = () => {
|
||||
browserWindow.webContents.insertCSS(cssToInject);
|
||||
};
|
||||
const onHeadersReceived = (details, callback) => {
|
||||
injectCss();
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||
};
|
||||
|
||||
browserWindow.webContents.on('did-finish-load', () => {
|
||||
// remove the injection of css the moment the page is loaded
|
||||
browserWindow.webContents.session.webRequest.onHeadersReceived(null);
|
||||
});
|
||||
|
||||
// on every page navigation inject the css
|
||||
browserWindow.webContents.on('did-navigate', () => {
|
||||
// we have to inject the css in onHeadersReceived so they're early enough
|
||||
// will run multiple times, so did-finish-load will remove this handler
|
||||
// We must inject css early enough; so onHeadersReceived is a good place.
|
||||
// Will run multiple times, see `did-finish-load` below that unsets this handler.
|
||||
browserWindow.webContents.session.webRequest.onHeadersReceived(
|
||||
{ urls: [] }, // Pass an empty filter list; null will not match _any_ urls
|
||||
onHeadersReceived,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function clearCache(browserWindow, targetUrl = null) {
|
||||
const { session } = browserWindow.webContents;
|
||||
session.clearStorageData(() => {
|
||||
session.clearCache(() => {
|
||||
if (targetUrl) {
|
||||
browserWindow.loadURL(targetUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setProxyRules(browserWindow, proxyRules) {
|
||||
browserWindow.webContents.session.setProxy(
|
||||
{
|
||||
proxyRules,
|
||||
(details, callback) => {
|
||||
browserWindow.webContents.insertCSS(cssToInject);
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function clearCache(browserWindow: BrowserWindow): Promise<void> {
|
||||
const { session } = browserWindow.webContents;
|
||||
await session.clearStorageData();
|
||||
await session.clearCache();
|
||||
}
|
||||
|
||||
function setProxyRules(browserWindow: BrowserWindow, proxyRules): void {
|
||||
browserWindow.webContents.session.setProxy({
|
||||
proxyRules,
|
||||
pacScript: '',
|
||||
proxyBypassRules: '',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{}} inpOptions AppArgs from nativefier.json
|
||||
* @param {{}} nativefierOptions AppArgs from nativefier.json
|
||||
* @param {function} onAppQuit
|
||||
* @param {function} setDockBadge
|
||||
* @returns {electron.BrowserWindow}
|
||||
*/
|
||||
function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
const options = Object.assign({}, inpOptions);
|
||||
export function createMainWindow(
|
||||
nativefierOptions,
|
||||
onAppQuit,
|
||||
setDockBadge,
|
||||
): BrowserWindow {
|
||||
const options = { ...nativefierOptions };
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: options.width || 1280,
|
||||
defaultHeight: options.height || 800,
|
||||
@ -105,19 +93,16 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
webPreferences: {
|
||||
javascript: true,
|
||||
plugins: true,
|
||||
// node globals causes problems with sites like messenger.com
|
||||
nodeIntegration: false,
|
||||
nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com
|
||||
webSecurity: !options.insecure,
|
||||
preload: path.join(__dirname, 'static', 'preload.js'),
|
||||
preload: path.join(__dirname, '..', 'preload.js'),
|
||||
zoomFactor: options.zoom,
|
||||
},
|
||||
};
|
||||
|
||||
const browserwindowOptions = Object.assign({}, options.browserwindowOptions);
|
||||
const browserwindowOptions = { ...options.browserwindowOptions };
|
||||
|
||||
const mainWindow = new BrowserWindow(
|
||||
Object.assign(
|
||||
{
|
||||
const mainWindow = new BrowserWindow({
|
||||
frame: !options.hideWindowFrame,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
@ -128,7 +113,6 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
x: options.x,
|
||||
y: options.y,
|
||||
autoHideMenuBar: !options.showMenuBar,
|
||||
// after webpack path here should reference `resources/app/`
|
||||
icon: getAppIcon(),
|
||||
// set to undefined and not false because explicitly setting to false will disable full screen
|
||||
fullscreen: options.fullScreen || undefined,
|
||||
@ -137,11 +121,9 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
titleBarStyle: options.titleBarStyle,
|
||||
show: options.tray !== 'start-in-tray',
|
||||
backgroundColor: options.backgroundColor,
|
||||
},
|
||||
DEFAULT_WINDOW_OPTIONS,
|
||||
browserwindowOptions,
|
||||
),
|
||||
);
|
||||
...DEFAULT_WINDOW_OPTIONS,
|
||||
...browserwindowOptions,
|
||||
});
|
||||
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
@ -151,7 +133,7 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
options.maximize = undefined;
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '..', 'nativefier.json'),
|
||||
path.join(__dirname, '../..', 'nativefier.json'),
|
||||
JSON.stringify(options),
|
||||
);
|
||||
} catch (exception) {
|
||||
@ -160,7 +142,7 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
}
|
||||
}
|
||||
|
||||
const withFocusedWindow = (block) => {
|
||||
const withFocusedWindow = (block: (window: BrowserWindow) => void): void => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
return block(focusedWindow);
|
||||
@ -168,75 +150,85 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const adjustWindowZoom = (window, adjustment) => {
|
||||
window.webContents.getZoomFactor((zoomFactor) => {
|
||||
window.webContents.setZoomFactor(zoomFactor + adjustment);
|
||||
});
|
||||
const adjustWindowZoom = (window: BrowserWindow, adjustment): void => {
|
||||
window.webContents.zoomFactor = window.webContents.zoomFactor + adjustment;
|
||||
};
|
||||
|
||||
const onZoomIn = () => {
|
||||
withFocusedWindow((focusedWindow) =>
|
||||
const onZoomIn = (): void => {
|
||||
withFocusedWindow((focusedWindow: BrowserWindow) =>
|
||||
adjustWindowZoom(focusedWindow, ZOOM_INTERVAL),
|
||||
);
|
||||
};
|
||||
|
||||
const onZoomOut = () => {
|
||||
withFocusedWindow((focusedWindow) =>
|
||||
const onZoomOut = (): void => {
|
||||
withFocusedWindow((focusedWindow: BrowserWindow) =>
|
||||
adjustWindowZoom(focusedWindow, -ZOOM_INTERVAL),
|
||||
);
|
||||
};
|
||||
|
||||
const onZoomReset = () => {
|
||||
withFocusedWindow((focusedWindow) => {
|
||||
focusedWindow.webContents.setZoomFactor(options.zoom);
|
||||
const onZoomReset = (): void => {
|
||||
withFocusedWindow((focusedWindow: BrowserWindow) => {
|
||||
focusedWindow.webContents.zoomFactor = options.zoom;
|
||||
});
|
||||
};
|
||||
|
||||
const clearAppData = () => {
|
||||
dialog.showMessageBox(
|
||||
mainWindow,
|
||||
{
|
||||
const clearAppData = async (): Promise<void> => {
|
||||
const response = await dialog.showMessageBox(mainWindow, {
|
||||
type: 'warning',
|
||||
buttons: ['Yes', 'Cancel'],
|
||||
defaultId: 1,
|
||||
title: 'Clear cache confirmation',
|
||||
message:
|
||||
'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?',
|
||||
},
|
||||
(response) => {
|
||||
if (response !== 0) {
|
||||
});
|
||||
|
||||
if (response.response !== 0) {
|
||||
return;
|
||||
}
|
||||
clearCache(mainWindow, options.targetUrl);
|
||||
},
|
||||
);
|
||||
await clearCache(mainWindow);
|
||||
};
|
||||
|
||||
const onGoBack = () => {
|
||||
const onGoBack = (): void => {
|
||||
withFocusedWindow((focusedWindow) => {
|
||||
focusedWindow.webContents.goBack();
|
||||
});
|
||||
};
|
||||
|
||||
const onGoForward = () => {
|
||||
const onGoForward = (): void => {
|
||||
withFocusedWindow((focusedWindow) => {
|
||||
focusedWindow.webContents.goForward();
|
||||
});
|
||||
};
|
||||
|
||||
const getCurrentUrl = () =>
|
||||
const getCurrentUrl = (): void =>
|
||||
withFocusedWindow((focusedWindow) => focusedWindow.webContents.getURL());
|
||||
|
||||
const onWillNavigate = (event, urlToGo) => {
|
||||
const onWillNavigate = (event: Event, urlToGo: string): void => {
|
||||
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(urlToGo);
|
||||
}
|
||||
};
|
||||
|
||||
let createNewWindow;
|
||||
const createNewWindow: (url: string) => BrowserWindow = (url: string) => {
|
||||
const window = new BrowserWindow(DEFAULT_WINDOW_OPTIONS);
|
||||
if (options.userAgent) {
|
||||
window.webContents.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
const createNewTab = (url, foreground) => {
|
||||
if (options.proxyRules) {
|
||||
setProxyRules(window, options.proxyRules);
|
||||
}
|
||||
|
||||
injectCss(window);
|
||||
sendParamsOnDidFinishLoad(window);
|
||||
window.webContents.on('new-window', onNewWindow);
|
||||
window.webContents.on('will-navigate', onWillNavigate);
|
||||
window.loadURL(url);
|
||||
return window;
|
||||
};
|
||||
|
||||
const createNewTab = (url: string, foreground: boolean): BrowserWindow => {
|
||||
withFocusedWindow((focusedWindow) => {
|
||||
const newTab = createNewWindow(url);
|
||||
focusedWindow.addTabbedWindow(newTab);
|
||||
@ -248,7 +240,7 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const createAboutBlankWindow = () => {
|
||||
const createAboutBlankWindow = (): BrowserWindow => {
|
||||
const window = createNewWindow('about:blank');
|
||||
window.hide();
|
||||
window.webContents.once('did-stop-loading', () => {
|
||||
@ -261,11 +253,15 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
return window;
|
||||
};
|
||||
|
||||
const onNewWindow = (event, urlToGo, _, disposition) => {
|
||||
const preventDefault = (newGuest) => {
|
||||
const onNewWindow = (
|
||||
event: Event & { newGuest?: any },
|
||||
urlToGo: string,
|
||||
frameName: string,
|
||||
disposition,
|
||||
): void => {
|
||||
const preventDefault = (newGuest: any): void => {
|
||||
event.preventDefault();
|
||||
if (newGuest) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.newGuest = newGuest;
|
||||
}
|
||||
};
|
||||
@ -275,37 +271,24 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
options.targetUrl,
|
||||
options.internalUrls,
|
||||
preventDefault,
|
||||
shell.openExternal,
|
||||
shell.openExternal.bind(this),
|
||||
createAboutBlankWindow,
|
||||
nativeTabsSupported,
|
||||
createNewTab,
|
||||
);
|
||||
};
|
||||
|
||||
const sendParamsOnDidFinishLoad = (window) => {
|
||||
const sendParamsOnDidFinishLoad = (window: BrowserWindow): void => {
|
||||
window.webContents.on('did-finish-load', () => {
|
||||
// In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron.
|
||||
// See https://github.com/jiahaog/nativefier/issues/379#issuecomment-598612128
|
||||
// and https://github.com/electron/electron/pull/12679
|
||||
window.webContents.setVisualZoomLevelLimits(1, 3);
|
||||
|
||||
window.webContents.send('params', JSON.stringify(options));
|
||||
});
|
||||
};
|
||||
|
||||
createNewWindow = (url) => {
|
||||
const window = new BrowserWindow(DEFAULT_WINDOW_OPTIONS);
|
||||
if (options.userAgent) {
|
||||
window.webContents.setUserAgent(options.userAgent);
|
||||
}
|
||||
|
||||
if (options.proxyRules) {
|
||||
setProxyRules(window, options.proxyRules);
|
||||
}
|
||||
|
||||
maybeInjectCss(window);
|
||||
sendParamsOnDidFinishLoad(window);
|
||||
window.webContents.on('new-window', onNewWindow);
|
||||
window.webContents.on('will-navigate', onWillNavigate);
|
||||
window.loadURL(url);
|
||||
return window;
|
||||
};
|
||||
|
||||
const menuOptions = {
|
||||
nativefierVersion: options.nativefierVersion,
|
||||
appQuit: onAppQuit,
|
||||
@ -329,14 +312,14 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
}
|
||||
|
||||
if (options.userAgent) {
|
||||
mainWindow.webContents.setUserAgent(options.userAgent);
|
||||
mainWindow.webContents.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
if (options.proxyRules) {
|
||||
setProxyRules(mainWindow, options.proxyRules);
|
||||
}
|
||||
|
||||
maybeInjectCss(mainWindow);
|
||||
injectCss(mainWindow);
|
||||
sendParamsOnDidFinishLoad(mainWindow);
|
||||
|
||||
if (options.counter) {
|
||||
@ -366,6 +349,15 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
|
||||
mainWindow.webContents.on('new-window', onNewWindow);
|
||||
mainWindow.webContents.on('will-navigate', onWillNavigate);
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
// Restore pinch-to-zoom, disabled by default in recent Electron.
|
||||
// See https://github.com/jiahaog/nativefier/issues/379#issuecomment-598309817
|
||||
// and https://github.com/electron/electron/pull/12679
|
||||
mainWindow.webContents.setVisualZoomLevelLimits(1, 3);
|
||||
|
||||
// Remove potential css injection code set in `did-navigate`) (see injectCss code)
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived(null);
|
||||
});
|
||||
|
||||
if (options.clearCache) {
|
||||
clearCache(mainWindow);
|
||||
@ -373,6 +365,7 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
|
||||
mainWindow.loadURL(options.targetUrl);
|
||||
|
||||
// @ts-ignore
|
||||
mainWindow.on('new-tab', () => createNewTab(options.targetUrl, true));
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
@ -383,10 +376,10 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
mainWindow.setFullScreen(false);
|
||||
mainWindow.once(
|
||||
'leave-full-screen',
|
||||
maybeHideWindow.bind(this, mainWindow, event, options.fastQuit),
|
||||
hideWindow.bind(this, mainWindow, event, options.fastQuit),
|
||||
);
|
||||
}
|
||||
maybeHideWindow(mainWindow, event, options.fastQuit, options.tray);
|
||||
hideWindow(mainWindow, event, options.fastQuit, options.tray);
|
||||
|
||||
if (options.clearCache) {
|
||||
clearCache(mainWindow);
|
||||
@ -395,5 +388,3 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) {
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
export default createMainWindow;
|
@ -1,6 +1,4 @@
|
||||
import mainWindowHelpers from './mainWindowHelpers';
|
||||
|
||||
const { onNewWindowHelper } = mainWindowHelpers;
|
||||
import { onNewWindowHelper } from './mainWindowHelpers';
|
||||
|
||||
const originalUrl = 'https://medium.com/';
|
||||
const internalUrl = 'https://medium.com/topics/technology';
|
@ -1,18 +1,16 @@
|
||||
import helpers from '../../helpers/helpers';
|
||||
import { linkIsInternal } from '../helpers/helpers';
|
||||
|
||||
const { linkIsInternal } = helpers;
|
||||
|
||||
function onNewWindowHelper(
|
||||
urlToGo,
|
||||
export function onNewWindowHelper(
|
||||
urlToGo: string,
|
||||
disposition,
|
||||
targetUrl,
|
||||
targetUrl: string,
|
||||
internalUrls,
|
||||
preventDefault,
|
||||
openExternal,
|
||||
createAboutBlankWindow,
|
||||
nativeTabsSupported,
|
||||
createNewTab,
|
||||
) {
|
||||
): void {
|
||||
if (!linkIsInternal(targetUrl, urlToGo, internalUrls)) {
|
||||
openExternal(urlToGo);
|
||||
preventDefault();
|
||||
@ -29,5 +27,3 @@ function onNewWindowHelper(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default { onNewWindowHelper };
|
@ -1,19 +1,6 @@
|
||||
import { Menu, shell, clipboard } from 'electron';
|
||||
import { Menu, clipboard, globalShortcut, shell } from 'electron';
|
||||
|
||||
/**
|
||||
* @param nativefierVersion
|
||||
* @param appQuit
|
||||
* @param zoomIn
|
||||
* @param zoomOut
|
||||
* @param zoomReset
|
||||
* @param zoomBuildTimeValue
|
||||
* @param goBack
|
||||
* @param goForward
|
||||
* @param getCurrentUrl
|
||||
* @param clearAppData
|
||||
* @param disableDevTools
|
||||
*/
|
||||
function createMenu({
|
||||
export function createMenu({
|
||||
nativefierVersion,
|
||||
appQuit,
|
||||
zoomIn,
|
||||
@ -25,13 +12,13 @@ function createMenu({
|
||||
getCurrentUrl,
|
||||
clearAppData,
|
||||
disableDevTools,
|
||||
}) {
|
||||
}): void {
|
||||
const zoomResetLabel =
|
||||
zoomBuildTimeValue === 1.0
|
||||
? 'Reset Zoom'
|
||||
: `Reset Zoom (to ${zoomBuildTimeValue * 100}%, set at build time)`;
|
||||
|
||||
const template = [
|
||||
const template: any[] = [
|
||||
{
|
||||
label: '&Edit',
|
||||
submenu: [
|
||||
@ -83,9 +70,7 @@ function createMenu({
|
||||
},
|
||||
{
|
||||
label: 'Clear App Data',
|
||||
click: () => {
|
||||
clearAppData();
|
||||
},
|
||||
click: clearAppData,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -94,17 +79,19 @@ function createMenu({
|
||||
submenu: [
|
||||
{
|
||||
label: 'Back',
|
||||
accelerator: 'CmdOrCtrl+[',
|
||||
click: () => {
|
||||
goBack();
|
||||
},
|
||||
accelerator: (() => {
|
||||
globalShortcut.register('Alt+Left', goBack);
|
||||
return 'CmdOrCtrl+[';
|
||||
})(),
|
||||
click: goBack,
|
||||
},
|
||||
{
|
||||
label: 'Forward',
|
||||
accelerator: 'CmdOrCtrl+]',
|
||||
click: () => {
|
||||
goForward();
|
||||
},
|
||||
accelerator: (() => {
|
||||
globalShortcut.register('Alt+Right', goForward);
|
||||
return 'CmdOrCtrl+]';
|
||||
})(),
|
||||
click: goForward,
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
@ -122,7 +109,7 @@ function createMenu({
|
||||
label: 'Toggle Full Screen',
|
||||
accelerator: (() => {
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Ctrl+Command+F';
|
||||
return 'Ctrl+Cmd+F';
|
||||
}
|
||||
return 'F11';
|
||||
})(),
|
||||
@ -135,44 +122,32 @@ function createMenu({
|
||||
{
|
||||
label: 'Zoom In',
|
||||
accelerator: (() => {
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Command+=';
|
||||
}
|
||||
return 'Ctrl+=';
|
||||
globalShortcut.register('CmdOrCtrl+numadd', zoomIn);
|
||||
return 'CmdOrCtrl+=';
|
||||
})(),
|
||||
click: () => {
|
||||
zoomIn();
|
||||
},
|
||||
click: zoomIn,
|
||||
},
|
||||
{
|
||||
label: 'Zoom Out',
|
||||
accelerator: (() => {
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Command+-';
|
||||
}
|
||||
return 'Ctrl+-';
|
||||
globalShortcut.register('CmdOrCtrl+numsub', zoomOut);
|
||||
return 'CmdOrCtrl+-';
|
||||
})(),
|
||||
click: () => {
|
||||
zoomOut();
|
||||
},
|
||||
click: zoomOut,
|
||||
},
|
||||
{
|
||||
label: zoomResetLabel,
|
||||
accelerator: (() => {
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Command+0';
|
||||
}
|
||||
return 'Ctrl+0';
|
||||
globalShortcut.register('CmdOrCtrl+num0', zoomReset);
|
||||
return 'CmdOrCtrl+0';
|
||||
})(),
|
||||
click: () => {
|
||||
zoomReset();
|
||||
},
|
||||
click: zoomReset,
|
||||
},
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator: (() => {
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Alt+Command+I';
|
||||
return 'Alt+Cmd+I';
|
||||
}
|
||||
return 'Ctrl+Shift+I';
|
||||
})(),
|
||||
@ -240,12 +215,12 @@ function createMenu({
|
||||
},
|
||||
{
|
||||
label: 'Hide App',
|
||||
accelerator: 'Command+H',
|
||||
accelerator: 'Cmd+H',
|
||||
role: 'hide',
|
||||
},
|
||||
{
|
||||
label: 'Hide Others',
|
||||
accelerator: 'Command+Shift+H',
|
||||
accelerator: 'Cmd+Shift+H',
|
||||
role: 'hideothers',
|
||||
},
|
||||
{
|
||||
@ -257,10 +232,8 @@ function createMenu({
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Command+Q',
|
||||
click: () => {
|
||||
appQuit();
|
||||
},
|
||||
accelerator: 'Cmd+Q',
|
||||
click: appQuit,
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -278,5 +251,3 @@ function createMenu({
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
export default createMenu;
|
@ -1,17 +1,12 @@
|
||||
import helpers from '../../helpers/helpers';
|
||||
import { app, Tray, Menu, ipcMain, nativeImage, BrowserWindow } from 'electron';
|
||||
|
||||
const { app, Tray, Menu, ipcMain, nativeImage } = require('electron');
|
||||
import { getAppIcon, getCounterValue } from '../helpers/helpers';
|
||||
|
||||
const { getAppIcon, getCounterValue } = helpers;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{}} inpOptions AppArgs from nativefier.json
|
||||
* @param {electron.BrowserWindow} mainWindow MainWindow created from main.js
|
||||
* @returns {electron.Tray}
|
||||
*/
|
||||
function createTrayIcon(inpOptions, mainWindow) {
|
||||
const options = Object.assign({}, inpOptions);
|
||||
export function createTrayIcon(
|
||||
nativefierOptions,
|
||||
mainWindow: BrowserWindow,
|
||||
): Tray {
|
||||
const options = { ...nativefierOptions };
|
||||
|
||||
if (options.tray) {
|
||||
const iconPath = getAppIcon();
|
||||
@ -33,7 +28,7 @@ function createTrayIcon(inpOptions, mainWindow) {
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
click: app.exit,
|
||||
click: app.exit.bind(this),
|
||||
},
|
||||
]);
|
||||
|
||||
@ -69,5 +64,3 @@ function createTrayIcon(inpOptions, mainWindow) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default createTrayIcon;
|
@ -1,87 +0,0 @@
|
||||
import wurl from 'wurl';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const INJECT_CSS_PATH = path.join(__dirname, '..', 'inject/inject.css');
|
||||
const log = require('loglevel');
|
||||
|
||||
function isOSX() {
|
||||
return os.platform() === 'darwin';
|
||||
}
|
||||
|
||||
function isLinux() {
|
||||
return os.platform() === 'linux';
|
||||
}
|
||||
|
||||
function isWindows() {
|
||||
return os.platform() === 'win32';
|
||||
}
|
||||
|
||||
function linkIsInternal(currentUrl, newUrl, internalUrlRegex) {
|
||||
if (newUrl === 'about:blank') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (internalUrlRegex) {
|
||||
const regex = RegExp(internalUrlRegex);
|
||||
return regex.test(newUrl);
|
||||
}
|
||||
|
||||
const currentDomain = wurl('domain', currentUrl);
|
||||
const newDomain = wurl('domain', newUrl);
|
||||
return currentDomain === newDomain;
|
||||
}
|
||||
|
||||
function shouldInjectCss() {
|
||||
try {
|
||||
fs.accessSync(INJECT_CSS_PATH, fs.F_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getCssToInject() {
|
||||
return fs.readFileSync(INJECT_CSS_PATH).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to print debug messages from the main process in the browser window
|
||||
* @param {BrowserWindow} browserWindow
|
||||
* @param message
|
||||
*/
|
||||
function debugLog(browserWindow, message) {
|
||||
// need the timeout as it takes time for the preload javascript to be loaded in the window
|
||||
setTimeout(() => {
|
||||
browserWindow.webContents.send('debug', message);
|
||||
}, 3000);
|
||||
log.info(message);
|
||||
}
|
||||
|
||||
function getAppIcon() {
|
||||
return path.join(__dirname, '../', `/icon.${isWindows() ? 'ico' : 'png'}`);
|
||||
}
|
||||
|
||||
function nativeTabsSupported() {
|
||||
return isOSX();
|
||||
}
|
||||
|
||||
function getCounterValue(title) {
|
||||
const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/;
|
||||
const match = itemCountRegex.exec(title);
|
||||
return match ? match[1] : undefined;
|
||||
}
|
||||
|
||||
export default {
|
||||
isOSX,
|
||||
isLinux,
|
||||
isWindows,
|
||||
linkIsInternal,
|
||||
getCssToInject,
|
||||
debugLog,
|
||||
shouldInjectCss,
|
||||
getAppIcon,
|
||||
nativeTabsSupported,
|
||||
getCounterValue,
|
||||
};
|
@ -1,6 +1,4 @@
|
||||
import helpers from './helpers';
|
||||
|
||||
const { linkIsInternal, getCounterValue } = helpers;
|
||||
import { linkIsInternal, getCounterValue } from './helpers';
|
||||
|
||||
const internalUrl = 'https://medium.com/';
|
||||
const internalUrlSubPath = 'topic/technology';
|
78
app/src/helpers/helpers.ts
Normal file
78
app/src/helpers/helpers.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
import * as log from 'loglevel';
|
||||
import wurl from 'wurl';
|
||||
|
||||
const INJECT_CSS_PATH = path.join(__dirname, '../..', 'inject/inject.css');
|
||||
|
||||
export function isOSX(): boolean {
|
||||
return os.platform() === 'darwin';
|
||||
}
|
||||
|
||||
export function isLinux(): boolean {
|
||||
return os.platform() === 'linux';
|
||||
}
|
||||
|
||||
export function isWindows(): boolean {
|
||||
return os.platform() === 'win32';
|
||||
}
|
||||
|
||||
export function linkIsInternal(
|
||||
currentUrl: string,
|
||||
newUrl: string,
|
||||
internalUrlRegex: string | RegExp,
|
||||
): boolean {
|
||||
if (newUrl === 'about:blank') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (internalUrlRegex) {
|
||||
const regex = RegExp(internalUrlRegex);
|
||||
return regex.test(newUrl);
|
||||
}
|
||||
|
||||
const currentDomain = wurl('domain', currentUrl);
|
||||
const newDomain = wurl('domain', newUrl);
|
||||
return currentDomain === newDomain;
|
||||
}
|
||||
|
||||
export function shouldInjectCss(): boolean {
|
||||
try {
|
||||
fs.accessSync(INJECT_CSS_PATH);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCssToInject(): string {
|
||||
return fs.readFileSync(INJECT_CSS_PATH).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to print debug messages from the main process in the browser window
|
||||
*/
|
||||
export function debugLog(browserWindow: BrowserWindow, message: string): void {
|
||||
// Need a delay, as it takes time for the preloaded js to be loaded by the window
|
||||
setTimeout(() => {
|
||||
browserWindow.webContents.send('debug', message);
|
||||
}, 3000);
|
||||
log.info(message);
|
||||
}
|
||||
|
||||
export function getAppIcon(): string {
|
||||
return path.join(__dirname, `../../icon.${isWindows() ? 'ico' : 'png'}`);
|
||||
}
|
||||
|
||||
export function nativeTabsSupported(): boolean {
|
||||
return isOSX();
|
||||
}
|
||||
|
||||
export function getCounterValue(title: string): string {
|
||||
const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/;
|
||||
const match = itemCountRegex.exec(title);
|
||||
return match ? match[1] : undefined;
|
||||
}
|
@ -1,31 +1,32 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import helpers from './helpers';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { isOSX, isWindows, isLinux } from './helpers';
|
||||
|
||||
const { isOSX, isWindows, isLinux } = helpers;
|
||||
const log = require('loglevel');
|
||||
/**
|
||||
* Synchronously find a file or directory
|
||||
* @param {RegExp} pattern regex
|
||||
* @param {string} base path
|
||||
* @param {boolean} [findDir] if true, search results will be limited to only directories
|
||||
* @returns {Array}
|
||||
* Find a file or directory
|
||||
*/
|
||||
function findSync(pattern, basePath, findDir) {
|
||||
const matches = [];
|
||||
function findSync(
|
||||
pattern: RegExp,
|
||||
basePath: string,
|
||||
limitSearchToDirectories = false,
|
||||
): string[] {
|
||||
const matches: string[] = [];
|
||||
|
||||
(function findSyncRecurse(base) {
|
||||
let children;
|
||||
let children: string[];
|
||||
try {
|
||||
children = fs.readdirSync(base);
|
||||
} catch (exception) {
|
||||
if (exception.code === 'ENOENT') {
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
throw exception;
|
||||
throw err;
|
||||
}
|
||||
|
||||
children.forEach((child) => {
|
||||
for (const child of children) {
|
||||
const childPath = path.join(base, child);
|
||||
const childIsDirectory = fs.lstatSync(childPath).isDirectory();
|
||||
const patternMatches = pattern.test(childPath);
|
||||
@ -38,7 +39,7 @@ function findSync(pattern, basePath, findDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!findDir) {
|
||||
if (!limitSearchToDirectories) {
|
||||
matches.push(childPath);
|
||||
return;
|
||||
}
|
||||
@ -46,23 +47,23 @@ function findSync(pattern, basePath, findDir) {
|
||||
if (childIsDirectory) {
|
||||
matches.push(childPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
})(basePath);
|
||||
return matches;
|
||||
}
|
||||
|
||||
function linuxMatch() {
|
||||
function findFlashOnLinux() {
|
||||
return findSync(/libpepflashplayer\.so/, '/opt/google/chrome')[0];
|
||||
}
|
||||
|
||||
function windowsMatch() {
|
||||
function findFlashOnWindows() {
|
||||
return findSync(
|
||||
/pepflashplayer\.dll/,
|
||||
'C:\\Program Files (x86)\\Google\\Chrome',
|
||||
)[0];
|
||||
}
|
||||
|
||||
function darwinMatch() {
|
||||
function findFlashOnMac() {
|
||||
return findSync(
|
||||
/PepperFlashPlayer.plugin/,
|
||||
'/Applications/Google Chrome.app/',
|
||||
@ -70,20 +71,19 @@ function darwinMatch() {
|
||||
)[0];
|
||||
}
|
||||
|
||||
function inferFlash() {
|
||||
export function inferFlashPath() {
|
||||
if (isOSX()) {
|
||||
return darwinMatch();
|
||||
return findFlashOnMac();
|
||||
}
|
||||
|
||||
if (isWindows()) {
|
||||
return windowsMatch();
|
||||
return findFlashOnWindows();
|
||||
}
|
||||
|
||||
if (isLinux()) {
|
||||
return linuxMatch();
|
||||
return findFlashOnLinux();
|
||||
}
|
||||
|
||||
log.warn('Unable to determine OS to infer flash player');
|
||||
return null;
|
||||
}
|
||||
export default inferFlash;
|
@ -1,29 +1,26 @@
|
||||
import 'source-map-support/register';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { app, crashReporter, globalShortcut } from 'electron';
|
||||
import electronDownload from 'electron-dl';
|
||||
|
||||
import createLoginWindow from './components/login/loginWindow';
|
||||
import createMainWindow from './components/mainWindow/mainWindow';
|
||||
import createTrayIcon from './components/trayIcon/trayIcon';
|
||||
import helpers from './helpers/helpers';
|
||||
import inferFlash from './helpers/inferFlash';
|
||||
import { createLoginWindow } from './components/loginWindow';
|
||||
import { createMainWindow } from './components/mainWindow';
|
||||
import { createTrayIcon } from './components/trayIcon';
|
||||
import { isOSX } from './helpers/helpers';
|
||||
import { inferFlashPath } from './helpers/inferFlash';
|
||||
|
||||
const electronSquirrelStartup = require('electron-squirrel-startup');
|
||||
|
||||
// Entrypoint for electron-squirrel-startup.
|
||||
// See https://github.com/jiahaog/nativefier/pull/744 for sample use case
|
||||
if (electronSquirrelStartup) {
|
||||
// Entrypoint for Squirrel, a windows update framework. See https://github.com/jiahaog/nativefier/pull/744
|
||||
if (require('electron-squirrel-startup')) {
|
||||
app.exit();
|
||||
}
|
||||
|
||||
const { isOSX } = helpers;
|
||||
|
||||
const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
||||
const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'));
|
||||
|
||||
const fileDownloadOptions = Object.assign({}, appArgs.fileDownloadOptions);
|
||||
const fileDownloadOptions = { ...appArgs.fileDownloadOptions };
|
||||
electronDownload(fileDownloadOptions);
|
||||
|
||||
if (appArgs.processEnvs) {
|
||||
@ -38,7 +35,7 @@ let mainWindow;
|
||||
if (typeof appArgs.flashPluginDir === 'string') {
|
||||
app.commandLine.appendSwitch('ppapi-flash-path', appArgs.flashPluginDir);
|
||||
} else if (appArgs.flashPluginDir) {
|
||||
const flashPath = inferFlash();
|
||||
const flashPath = inferFlashPath();
|
||||
app.commandLine.appendSwitch('ppapi-flash-path', flashPath);
|
||||
}
|
||||
|
||||
@ -76,18 +73,15 @@ if (appArgs.basicAuthPassword) {
|
||||
);
|
||||
}
|
||||
|
||||
// do nothing for setDockBadge if not OSX
|
||||
let setDockBadge = () => {};
|
||||
|
||||
if (isOSX()) {
|
||||
let currentBadgeCount = 0;
|
||||
|
||||
setDockBadge = (count, bounce = false) => {
|
||||
app.dock.setBadge(count);
|
||||
const isRunningMacos = isOSX();
|
||||
let currentBadgeCount = 0;
|
||||
const setDockBadge = isRunningMacos
|
||||
? (count: number, bounce = false) => {
|
||||
app.dock.setBadge(count.toString());
|
||||
if (bounce && count > currentBadgeCount) app.dock.bounce();
|
||||
currentBadgeCount = count;
|
||||
};
|
||||
}
|
||||
}
|
||||
: () => undefined;
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (!isOSX() || appArgs.fastQuit) {
|
||||
@ -147,7 +141,7 @@ if (shouldQuit) {
|
||||
});
|
||||
|
||||
app.on('ready', () => {
|
||||
mainWindow = createMainWindow(appArgs, app.quit, setDockBadge);
|
||||
mainWindow = createMainWindow(appArgs, app.quit.bind(this), setDockBadge);
|
||||
createTrayIcon(appArgs, mainWindow);
|
||||
|
||||
// Register global shortcuts
|
@ -1,25 +1,19 @@
|
||||
/**
|
||||
Preload file that will be executed in the renderer process
|
||||
*/
|
||||
|
||||
/**
|
||||
* Note: This needs to be attached prior to the imports, as the they will delay
|
||||
* the attachment till after the event has been raised.
|
||||
* Preload file that will be executed in the renderer process.
|
||||
* Note: This needs to be attached **prior to imports**, as imports
|
||||
* would delay the attachment till after the event has been raised.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Due to the early attachment, this triggers a linter error
|
||||
// because it's not yet been defined.
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
injectScripts();
|
||||
injectScripts(); // eslint-disable-line @typescript-eslint/no-use-before-define
|
||||
});
|
||||
|
||||
// Disable imports being first due to the above event attachment
|
||||
import { ipcRenderer } from 'electron'; // eslint-disable-line import/first
|
||||
import path from 'path'; // eslint-disable-line import/first
|
||||
import fs from 'fs'; // eslint-disable-line import/first
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const INJECT_JS_PATH = path.join(__dirname, '../../', 'inject/inject.js');
|
||||
const log = require('loglevel');
|
||||
import { ipcRenderer } from 'electron';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
const INJECT_JS_PATH = path.join(__dirname, '..', 'inject/inject.js');
|
||||
/**
|
||||
* Patches window.Notification to:
|
||||
* - set a callback on a new Notification
|
||||
@ -40,6 +34,7 @@ function setNotificationCallback(createCallback, clickCallback) {
|
||||
get: () => OldNotify.permission,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.Notification = newNotify;
|
||||
}
|
||||
|
||||
@ -49,7 +44,6 @@ function injectScripts() {
|
||||
return;
|
||||
}
|
||||
// Dynamically require scripts
|
||||
// eslint-disable-next-line global-require, import/no-dynamic-require
|
||||
require(INJECT_JS_PATH);
|
||||
}
|
||||
|
||||
@ -68,6 +62,5 @@ ipcRenderer.on('params', (event, message) => {
|
||||
});
|
||||
|
||||
ipcRenderer.on('debug', (event, message) => {
|
||||
// eslint-disable-next-line no-console
|
||||
log.info('debug:', message);
|
||||
});
|
10
app/src/static/login.js
Normal file
10
app/src/static/login.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const usernameInput = document.getElementById('username-input');
|
||||
const username = usernameInput.nodeValue || usernameInput.value;
|
||||
const passwordInput = document.getElementById('password-input');
|
||||
const password = passwordInput.nodeValue || passwordInput.value;
|
||||
ipcRenderer.send('login-message', [username, password]);
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
import electron from 'electron';
|
||||
|
||||
const { ipcRenderer } = electron;
|
||||
|
||||
const form = document.getElementById('login-form');
|
||||
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const username = document.getElementById('username-input').value;
|
||||
const password = document.getElementById('password-input').value;
|
||||
ipcRenderer.send('login-message', [username, password]);
|
||||
});
|
19
app/tsconfig.json
Normal file
19
app/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"declaration": false,
|
||||
"esModuleInterop": true,
|
||||
"incremental": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./lib",
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"target": "es2017",
|
||||
"lib": ["es2017", "dom"]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
# Development
|
||||
# Development Guide
|
||||
|
||||
## Environment Setup
|
||||
## Setup
|
||||
|
||||
First, clone the project
|
||||
|
||||
@ -9,49 +9,59 @@ git clone https://github.com/jiahaog/nativefier.git
|
||||
cd nativefier
|
||||
```
|
||||
|
||||
Install dependencies and build:
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
# macOS and Linux
|
||||
npm run dev-up
|
||||
|
||||
# Windows
|
||||
npm run dev-up-win
|
||||
npm install
|
||||
```
|
||||
|
||||
If dependencies are installed and you just want to re-build,
|
||||
Build nativefier:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can set up a symbolic link so that running `nativefier` invokes your development version including your changes:
|
||||
Set up a symbolic link so that running `nativefier` calls your dev version with your changes:
|
||||
|
||||
```bash
|
||||
npm link
|
||||
which nativefier
|
||||
# -> Should return a path, e.g. /home/youruser/.node_modules/lib/node_modules/nativefier
|
||||
# If not, be sure your `npm_config_prefix` env var is set and in your `PATH`
|
||||
```
|
||||
|
||||
After doing so (and not forgetting to build with `npm run build`), you can run Nativefier with your test parameters:
|
||||
After doing so, you can run Nativefier with your test parameters:
|
||||
|
||||
```bash
|
||||
nativefier <--your-awesome-new-flag>
|
||||
nativefier --your-awesome-new-flag 'https://your-test-site.com'
|
||||
```
|
||||
|
||||
Or you can automatically watch the files for changes with:
|
||||
|
||||
Then run your nativefier app *through the command line too* (to see logs & errors):
|
||||
```bash
|
||||
npm run watch
|
||||
# Under Linux
|
||||
./your-test-site-linux-x64/your-test-site
|
||||
|
||||
# Under Windows
|
||||
your-test-site-win32-x64/your-test-site.exe
|
||||
|
||||
# Under macOS
|
||||
open -a YourTestSite.app
|
||||
```
|
||||
|
||||
## Linting & formatting
|
||||
|
||||
Nativefier uses [Prettier](https://prettier.io/), which will shout at you for
|
||||
not formatting code exactly like it expects. This guarantees a homogenous style,
|
||||
but is painful to do manually. Do yourself a favor and install a
|
||||
[Prettier plugin for your editor](https://prettier.io/docs/en/editors.html).
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
# To run all tests (unit, end-to-end),
|
||||
npm test
|
||||
|
||||
# To run only unit tests,
|
||||
npm run jest
|
||||
|
||||
# To run only end-to-end tests,
|
||||
npm run e2e
|
||||
```
|
||||
- To run all tests, `npm t`
|
||||
- To run only unit tests, `npm run test:unit`
|
||||
- To run only integration tests, `npm run test:integration`
|
||||
- Logging is suppressed by default in tests, to avoid polluting Jest output.
|
||||
To get debug logs, `npm run test:withlog` or set the `LOGLEVEL` env. var.
|
||||
- For a good live experience, open two terminal panes/tabs running code/tests watchers:
|
||||
2. Run a TSC watcher: `npm run build:watch`
|
||||
3. Run a Jest unit tests watcher: `npm run test:watch`
|
||||
|
BIN
docs/dock.png
Normal file
BIN
docs/dock.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@ -43,9 +43,9 @@ mv package.json.tmp package.json
|
||||
|
||||
# Unset the editor so that git changelog does not open a editor
|
||||
EDITOR=:
|
||||
git changelog docs/changelog.md --tag "$VERSION"
|
||||
git changelog CHANGELOG.md --tag "$VERSION"
|
||||
|
||||
# Commit these changes
|
||||
git add docs/changelog.md
|
||||
git add CHANGELOG.md
|
||||
git add package.json
|
||||
git commit -m "Update changelog for \`v$VERSION\`"
|
61
docs/manual-test
Executable file
61
docs/manual-test
Executable file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Manual test to validate some hard-to-programmatically-test features work.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
missingDeps=false
|
||||
if ! command -v mktemp > /dev/null; then echo "Missing mktemp"; missingDeps=true; fi
|
||||
if ! command -v uname > /dev/null; then echo "Missing uname"; missingDeps=true; fi
|
||||
if ! command -v node > /dev/null; then echo "Missing node"; missingDeps=true; fi
|
||||
if [ "$missingDeps" = true ]; then exit 1; fi
|
||||
|
||||
|
||||
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
nativefier_dir="$script_dir/.."
|
||||
pushd "$nativefier_dir"
|
||||
|
||||
printf "\n***** Creating test dirs & resources *****\n"
|
||||
tmp_dir=$(mktemp -d -t nativefier-manual-test-XXXXX)
|
||||
resources_dir="$tmp_dir/resources"
|
||||
mkdir "$resources_dir"
|
||||
injected_css="$resources_dir/inject.css"
|
||||
injected_js="$resources_dir/inject.js"
|
||||
echo '* { background-color: blue; }' > "$injected_css"
|
||||
echo 'alert("hello world from inject");' > "$injected_js"
|
||||
|
||||
printf "\n***** Building test app *****\n"
|
||||
node ./lib/cli.js 'https://npmjs.com/' \
|
||||
--inject "$injected_css" \
|
||||
--inject "$injected_js" \
|
||||
--name "app" \
|
||||
"$tmp_dir"
|
||||
|
||||
printf "\n***** Test checklist *****
|
||||
- Injected js: should show an alert saying hello
|
||||
- Injected css: should make npmjs all blue
|
||||
- Internal links open internally
|
||||
- External links open in browser
|
||||
- Keyboard shortcuts: {back, forward, zoom in/out/zero} work
|
||||
- Console: no Electron runtime deprecation warnings/error logged
|
||||
"
|
||||
|
||||
printf "\n***** Running app *****\n"
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
open -a 'app-darwin-x64/app.app'
|
||||
else
|
||||
"$tmp_dir/app-linux-x64/app"
|
||||
fi
|
||||
|
||||
printf "\nDid everything work as expected? [yN] "
|
||||
read -r response
|
||||
if [ "$response" != 'y' ]; then
|
||||
echo "Back to fixing"
|
||||
exit 1
|
||||
else
|
||||
echo "Yayyyyyyyyyyy"
|
||||
fi
|
||||
|
||||
if [ -n "$tmp_dir" ]; then
|
||||
printf "\n***** Deleting test dir %s *****\n" "$tmp_dir"
|
||||
rm -rf "$tmp_dir"
|
||||
fi
|
@ -1,17 +1,28 @@
|
||||
# Release
|
||||
|
||||
Releases are automatically deployed to NPM on Travis, when they are tagged. However, we have to make sure that the version in the `package.json`, and the changelog is updated.
|
||||
Releases are automatically deployed to npm from Travis, when they are tagged.
|
||||
However, we have to make sure that the version in the `package.json`,
|
||||
and the changelog is updated.
|
||||
|
||||
## Dependencies
|
||||
- [Git Extras](https://github.com/tj/git-extras/blob/master/Installation.md)
|
||||
- [jq](https://stedolan.github.io/jq/download/)
|
||||
## Tests
|
||||
|
||||
## How to Release `$VERSION`
|
||||
Before anything, run a little manual smoke test of some of our
|
||||
hard-to-programatically-test features:
|
||||
|
||||
```bash
|
||||
npm run test:manual
|
||||
```
|
||||
|
||||
## How to release
|
||||
|
||||
With [Git Extras](https://github.com/tj/git-extras/blob/master/Installation.md)
|
||||
and [jq](https://stedolan.github.io/jq/download/) installed.
|
||||
|
||||
While on `master`, with no uncommitted changes,
|
||||
|
||||
```bash
|
||||
npm run changelog -- $VERSION
|
||||
# For example, npm run changelog -- 7.7.1
|
||||
```
|
||||
|
||||
This command does 3 things:
|
||||
@ -22,15 +33,17 @@ This command does 3 things:
|
||||
Now we may want to cleanup the changelog:
|
||||
|
||||
```bash
|
||||
vim docs/changelog.md
|
||||
|
||||
vim CHANGELOG.md
|
||||
git commit --amend
|
||||
```
|
||||
|
||||
Once we are satisfied,
|
||||
|
||||
```bash
|
||||
git push origin master
|
||||
```
|
||||
|
||||
On [GitHub Releases](https://github.com/jiahaog/nativefier/releases), draft and publish a new release with title `Nativefier vX.X.X`.
|
||||
On [GitHub Releases](https://github.com/jiahaog/nativefier/releases),
|
||||
draft and publish a new release with title `Nativefier vX.X.X` (yes, with a `v`).
|
||||
|
||||
The new version will be visible on npm within a few minutes/hours.
|
||||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
@ -1,83 +0,0 @@
|
||||
import tmp from 'tmp';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import async from 'async';
|
||||
|
||||
import nativefier from '../src';
|
||||
|
||||
const PLATFORMS = ['darwin', 'linux'];
|
||||
tmp.setGracefulCleanup();
|
||||
|
||||
function checkApp(appPath, inputOptions, callback) {
|
||||
try {
|
||||
let relPathToConfig;
|
||||
|
||||
switch (inputOptions.platform) {
|
||||
case 'darwin':
|
||||
relPathToConfig = path.join(
|
||||
'google-test-app.app',
|
||||
'Contents/Resources/app',
|
||||
);
|
||||
break;
|
||||
case 'linux':
|
||||
relPathToConfig = 'resources/app';
|
||||
break;
|
||||
case 'win32':
|
||||
relPathToConfig = 'resources/app';
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown app platform');
|
||||
}
|
||||
|
||||
const nativefierConfigPath = path.join(
|
||||
appPath,
|
||||
relPathToConfig,
|
||||
'nativefier.json',
|
||||
);
|
||||
const nativefierConfig = JSON.parse(fs.readFileSync(nativefierConfigPath));
|
||||
|
||||
expect(inputOptions.targetUrl).toBe(nativefierConfig.targetUrl);
|
||||
// app name is not consistent for linux
|
||||
// assert.strictEqual(inputOptions.appName, nativefierConfig.name,
|
||||
// 'Packaged app must have the same name as the input parameters');
|
||||
callback();
|
||||
} catch (exception) {
|
||||
callback(exception);
|
||||
}
|
||||
}
|
||||
|
||||
describe('Nativefier Module', () => {
|
||||
jest.setTimeout(240000);
|
||||
test('Can build an app from a target url', (done) => {
|
||||
async.eachSeries(
|
||||
PLATFORMS,
|
||||
(platform, callback) => {
|
||||
const tmpObj = tmp.dirSync({ unsafeCleanup: true });
|
||||
|
||||
const tmpPath = tmpObj.name;
|
||||
const options = {
|
||||
name: 'google-test-app',
|
||||
targetUrl: 'http://google.com',
|
||||
out: tmpPath,
|
||||
overwrite: true,
|
||||
platform: null,
|
||||
};
|
||||
|
||||
options.platform = platform;
|
||||
nativefier(options, (error, appPath) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
checkApp(appPath, options, (err) => {
|
||||
callback(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
done(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import del from 'del';
|
||||
import runSequence from 'run-sequence';
|
||||
import PATHS from './helpers/src-paths';
|
||||
|
||||
gulp.task('build', (callback) => {
|
||||
runSequence('clean', ['build-cli', 'build-app'], callback);
|
||||
});
|
||||
|
||||
gulp.task('clean', (callback) => {
|
||||
del(PATHS.CLI_DEST).then(() => {
|
||||
del(PATHS.APP_DEST).then(() => {
|
||||
del(PATHS.TEST_DEST).then(() => {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import webpack from 'webpack-stream';
|
||||
import PATHS from '../helpers/src-paths';
|
||||
|
||||
const webpackConfig = require('./../../webpack.config.js');
|
||||
|
||||
gulp.task('build-app', ['build-static'], () =>
|
||||
gulp
|
||||
.src(PATHS.APP_MAIN_JS)
|
||||
.pipe(webpack(webpackConfig))
|
||||
.pipe(gulp.dest(PATHS.APP_DEST)),
|
||||
);
|
@ -1,9 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import PATHS from '../helpers/src-paths';
|
||||
import helpers from '../helpers/gulp-helpers';
|
||||
|
||||
const { buildES6 } = helpers;
|
||||
|
||||
gulp.task('build-cli', (done) =>
|
||||
buildES6(PATHS.CLI_SRC_JS, PATHS.CLI_DEST, done),
|
||||
);
|
@ -1,17 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import PATHS from '../helpers/src-paths';
|
||||
import helpers from '../helpers/gulp-helpers';
|
||||
|
||||
const { buildES6 } = helpers;
|
||||
|
||||
gulp.task('build-static-not-js', () =>
|
||||
gulp
|
||||
.src([PATHS.APP_STATIC_ALL, '!**/*.js'])
|
||||
.pipe(gulp.dest(PATHS.APP_STATIC_DEST)),
|
||||
);
|
||||
|
||||
gulp.task('build-static-js', (done) =>
|
||||
buildES6(PATHS.APP_STATIC_JS, PATHS.APP_STATIC_DEST, done),
|
||||
);
|
||||
|
||||
gulp.task('build-static', ['build-static-js', 'build-static-not-js']);
|
@ -1,29 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import shellJs from 'shelljs';
|
||||
import sourcemaps from 'gulp-sourcemaps';
|
||||
import babel from 'gulp-babel';
|
||||
|
||||
function shellExec(cmd, silent, callback) {
|
||||
shellJs.exec(cmd, { silent }, (code, stdout, stderr) => {
|
||||
if (code) {
|
||||
callback(JSON.stringify({ code, stdout, stderr }));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function buildES6(src, dest, callback) {
|
||||
return gulp
|
||||
.src(src)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(babel())
|
||||
.on('error', callback)
|
||||
.pipe(sourcemaps.write('.'))
|
||||
.pipe(gulp.dest(dest));
|
||||
}
|
||||
|
||||
export default {
|
||||
shellExec,
|
||||
buildES6,
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
const paths = {
|
||||
APP_SRC: 'app/src',
|
||||
APP_DEST: 'app/lib',
|
||||
CLI_SRC: 'src',
|
||||
CLI_DEST: 'lib',
|
||||
TEST_SRC: 'test',
|
||||
TEST_DEST: 'built-tests',
|
||||
};
|
||||
|
||||
paths.APP_MAIN_JS = path.join(paths.APP_SRC, '/main.js');
|
||||
paths.APP_ALL = `${paths.APP_SRC}/**/*`;
|
||||
paths.APP_STATIC_ALL = `${path.join(paths.APP_SRC, 'static')}/**/*`;
|
||||
paths.APP_STATIC_JS = `${path.join(paths.APP_SRC, 'static')}/**/*.js`;
|
||||
paths.APP_STATIC_DEST = path.join(paths.APP_DEST, 'static');
|
||||
paths.CLI_SRC_JS = `${paths.CLI_SRC}/**/*.js`;
|
||||
paths.CLI_DEST_JS = `${paths.CLI_DEST}/**/*.js`;
|
||||
paths.TEST_SRC_JS = `${paths.TEST_SRC}/**/*.js`;
|
||||
paths.TEST_DEST_JS = `${paths.TEST_DEST}/**/*.js`;
|
||||
|
||||
export default paths;
|
@ -1,11 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import runSequence from 'run-sequence';
|
||||
import helpers from './helpers/gulp-helpers';
|
||||
|
||||
const { shellExec } = helpers;
|
||||
|
||||
gulp.task('publish', (done) => {
|
||||
shellExec('npm publish', false, done);
|
||||
});
|
||||
|
||||
gulp.task('release', (callback) => runSequence('build', 'publish', callback));
|
@ -1,13 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import PATHS from './helpers/src-paths';
|
||||
|
||||
const log = require('loglevel');
|
||||
|
||||
gulp.task('watch', ['build'], () => {
|
||||
const handleError = function watch(error) {
|
||||
log.error(error);
|
||||
};
|
||||
gulp.watch(PATHS.APP_ALL, ['build-app']).on('error', handleError);
|
||||
|
||||
gulp.watch(PATHS.CLI_SRC_JS, ['build-cli']).on('error', handleError);
|
||||
});
|
@ -1,9 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import requireDir from 'require-dir';
|
||||
|
||||
requireDir('./gulp', {
|
||||
recurse: true,
|
||||
duplicates: true,
|
||||
});
|
||||
|
||||
gulp.task('default', ['build']);
|
@ -8,8 +8,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
type convert >/dev/null 2>&1 || { echo >&2 "Cannot find required ImageMagick Convert executable"; exit 1; }
|
||||
type identify >/dev/null 2>&1 || { echo >&2 "Cannot find required ImageMagick Identify executable"; exit 1; }
|
||||
type convert >/dev/null 2>&1 || { echo >&2 "Cannot find required ImageMagick 'convert' executable, please install it and make sure it is in your PATH"; exit 1; }
|
||||
type identify >/dev/null 2>&1 || { echo >&2 "Cannot find required ImageMagick 'identify' executable, please install it and make sure it is in your PATH"; exit 1; }
|
||||
|
||||
# Parameters
|
||||
SOURCE="$1"
|
@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
};
|
146
package.json
146
package.json
@ -2,6 +2,12 @@
|
||||
"name": "nativefier",
|
||||
"version": "7.7.1",
|
||||
"description": "Wrap web apps natively",
|
||||
"license": "MIT",
|
||||
"author": "Goh Jia Hao",
|
||||
"engines": {
|
||||
"node": ">= 8.10.0",
|
||||
"npm": ">= 5.6.0"
|
||||
},
|
||||
"keywords": [
|
||||
"desktop",
|
||||
"electron",
|
||||
@ -9,97 +15,85 @@
|
||||
"native",
|
||||
"wrapper"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"dev-up": "npm install && (cd ./app && npm install) && npm run build",
|
||||
"dev-up-win": "npm install & cd app & npm install & cd .. & npm run build",
|
||||
"test": "jest src",
|
||||
"guard": "jest --watch src",
|
||||
"e2e": "jest e2e",
|
||||
"tdd": "gulp tdd",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"ci": "npm run lint && npm test && npm run e2e",
|
||||
"clean": "gulp clean",
|
||||
"build": "gulp build",
|
||||
"watch": "while true ; do gulp watch ; done",
|
||||
"package-placeholder": "npm run build && node lib/cli.js http://www.bennish.net/web-notifications.html ~/Desktop --overwrite --name notification-test --icon ./test-resources/iconSampleGrey.png --inject ./test-resources/test-injection.js --inject ./test-resources/test-injection.css && open ~/Desktop/notification-test-darwin-x64/notification-test.app",
|
||||
"start-placeholder": "npm run build && electron app",
|
||||
"changelog": "./scripts/changelog",
|
||||
"format": "prettier --write '{gulp,src}/**/*.js' 'app/src/**/*.js'"
|
||||
},
|
||||
"main": "lib/main.js",
|
||||
"bin": {
|
||||
"nativefier": "lib/cli.js"
|
||||
},
|
||||
"homepage": "https://github.com/jiahaog/nativefier",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jiahaog/nativefier.git"
|
||||
},
|
||||
"author": "Goh Jia Hao",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/jiahaog/nativefier/issues"
|
||||
},
|
||||
"homepage": "https://github.com/jiahaog/nativefier#readme",
|
||||
"scripts": {
|
||||
"build-app-static": "ncp app/src/static/ app/lib/static/",
|
||||
"build": "npm run clean && tsc --build . app && npm run build-app-static",
|
||||
"build:watch": "tsc --build . app --watch",
|
||||
"changelog": "./docs/generate-changelog",
|
||||
"ci": "npm run lint && npm test",
|
||||
"clean": "rimraf lib/ app/lib/",
|
||||
"clean:full": "rimraf lib/ app/lib/ node_modules/ app/node_modules/",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:format": "prettier --write 'src/**/*.js' 'app/src/**/*.js'",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"list-outdated-deps": "npm out; cd app && npm out; true",
|
||||
"postinstall": "cd app && yarn install --no-lockfile --no-progress --silent",
|
||||
"test:integration": "jest --testRegex '.*integration-test.js'",
|
||||
"test:manual": "npm run build && ./docs/manual-test",
|
||||
"test:unit": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:withlog": "LOGLEVEL=trace npm run test",
|
||||
"test": "jest --testRegex '[-.]test\\.js$'"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^2.6.0",
|
||||
"axios": "^0.18.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"commander": "^2.14.0",
|
||||
"electron-packager": "^12.2.0",
|
||||
"gitcloud": "^0.1.0",
|
||||
"hasbin": "^1.2.3",
|
||||
"lodash": "^4.17.5",
|
||||
"loglevel": "^1.6.1",
|
||||
"ncp": "^2.0.0",
|
||||
"page-icon": "^0.3.0",
|
||||
"progress": "^2.0.0",
|
||||
"sanitize-filename": "^1.6.1",
|
||||
"shelljs": "^0.8.1",
|
||||
"source-map-support": "^0.5.3",
|
||||
"tmp": "0.0.33",
|
||||
"validator": "^10.2.0"
|
||||
"@types/cheerio": "0.x",
|
||||
"@types/electron-packager": "14.x",
|
||||
"@types/lodash": "4.x",
|
||||
"@types/ncp": "2.x",
|
||||
"@types/node": "8.x",
|
||||
"@types/page-icon": "0.x",
|
||||
"@types/shelljs": "0.x",
|
||||
"@types/tmp": "0.x",
|
||||
"axios": "0.x",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"commander": "4.x",
|
||||
"electron-packager": "14.x",
|
||||
"gitcloud": "0.x",
|
||||
"hasbin": "1.x",
|
||||
"lodash": "4.x",
|
||||
"loglevel": "1.x",
|
||||
"ncp": "2.x",
|
||||
"page-icon": "0.x",
|
||||
"sanitize-filename": "1.x",
|
||||
"shelljs": "0.x",
|
||||
"source-map-support": "0.x",
|
||||
"tmp": "0.x",
|
||||
"yarn": "1.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-jest": "^23.4.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"chai": "^4.1.2",
|
||||
"del": "^3.0.0",
|
||||
"eslint": "^5.2.0",
|
||||
"eslint-config-airbnb-base": "^13.0.0",
|
||||
"eslint-config-prettier": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-prettier": "^3.0.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-babel": "^7.0.1",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"jest": "^23.4.1",
|
||||
"prettier": "^1.12.1",
|
||||
"require-dir": "^1.0.0",
|
||||
"run-sequence": "^2.2.1",
|
||||
"webpack-stream": "^5.0.0"
|
||||
"@types/jest": "25.x",
|
||||
"@typescript-eslint/eslint-plugin": "2.x",
|
||||
"@typescript-eslint/parser": "2.x",
|
||||
"eslint": "6.x",
|
||||
"eslint-config-prettier": "6.x",
|
||||
"eslint-plugin-prettier": "3.x",
|
||||
"jest": "25.x",
|
||||
"prettier": "1.x",
|
||||
"rimraf": "3.x",
|
||||
"typescript": "3.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
"jest": {
|
||||
"collectCoverage": true,
|
||||
"setupFiles": [
|
||||
"./lib/jestSetupFiles"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "4.0.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
"testEnvironment": "node",
|
||||
"testPathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"<rootDir>/app/src.*",
|
||||
"<rootDir>/src.*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
@ -1,163 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import ncp from 'ncp';
|
||||
|
||||
const copy = ncp.ncp;
|
||||
const log = require('loglevel');
|
||||
/**
|
||||
* Only picks certain app args to pass to nativefier.json
|
||||
* @param options
|
||||
*/
|
||||
function selectAppArgs(options) {
|
||||
return {
|
||||
name: options.name,
|
||||
targetUrl: options.targetUrl,
|
||||
counter: options.counter,
|
||||
bounce: options.bounce,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
minWidth: options.minWidth,
|
||||
minHeight: options.minHeight,
|
||||
maxWidth: options.maxWidth,
|
||||
maxHeight: options.maxHeight,
|
||||
x: options.x,
|
||||
y: options.y,
|
||||
showMenuBar: options.showMenuBar,
|
||||
fastQuit: options.fastQuit,
|
||||
userAgent: options.userAgent,
|
||||
nativefierVersion: options.nativefierVersion,
|
||||
ignoreCertificate: options.ignoreCertificate,
|
||||
disableGpu: options.disableGpu,
|
||||
ignoreGpuBlacklist: options.ignoreGpuBlacklist,
|
||||
enableEs3Apis: options.enableEs3Apis,
|
||||
insecure: options.insecure,
|
||||
flashPluginDir: options.flashPluginDir,
|
||||
diskCacheSize: options.diskCacheSize,
|
||||
fullScreen: options.fullScreen,
|
||||
hideWindowFrame: options.hideWindowFrame,
|
||||
maximize: options.maximize,
|
||||
disableContextMenu: options.disableContextMenu,
|
||||
disableDevTools: options.disableDevTools,
|
||||
zoom: options.zoom,
|
||||
internalUrls: options.internalUrls,
|
||||
proxyRules: options.proxyRules,
|
||||
crashReporter: options.crashReporter,
|
||||
singleInstance: options.singleInstance,
|
||||
clearCache: options.clearCache,
|
||||
appCopyright: options.appCopyright,
|
||||
appVersion: options.appVersion,
|
||||
buildVersion: options.buildVersion,
|
||||
win32metadata: options.win32metadata,
|
||||
versionString: options.versionString,
|
||||
processEnvs: options.processEnvs,
|
||||
fileDownloadOptions: options.fileDownloadOptions,
|
||||
tray: options.tray,
|
||||
basicAuthUsername: options.basicAuthUsername,
|
||||
basicAuthPassword: options.basicAuthPassword,
|
||||
alwaysOnTop: options.alwaysOnTop,
|
||||
titleBarStyle: options.titleBarStyle,
|
||||
globalShortcuts: options.globalShortcuts,
|
||||
browserwindowOptions: options.browserwindowOptions,
|
||||
backgroundColor: options.backgroundColor,
|
||||
darwinDarkModeSupport: options.darwinDarkModeSupport,
|
||||
};
|
||||
}
|
||||
|
||||
function maybeCopyScripts(srcs, dest) {
|
||||
if (!srcs) {
|
||||
return new Promise((resolve) => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
const promises = srcs.map(
|
||||
(src) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!fs.existsSync(src)) {
|
||||
reject(new Error('Error copying injection files: file not found'));
|
||||
return;
|
||||
}
|
||||
|
||||
let destFileName;
|
||||
if (path.extname(src) === '.js') {
|
||||
destFileName = 'inject.js';
|
||||
} else if (path.extname(src) === '.css') {
|
||||
destFileName = 'inject.css';
|
||||
} else {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
copy(src, path.join(dest, 'inject', destFileName), (error) => {
|
||||
if (error) {
|
||||
reject(new Error(`Error Copying injection files: ${error}`));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeAppName(appName, url) {
|
||||
// use a simple 3 byte random string to prevent collision
|
||||
const hash = crypto.createHash('md5');
|
||||
hash.update(url);
|
||||
const postFixHash = hash.digest('hex').substring(0, 6);
|
||||
const normalized = _.kebabCase(appName.toLowerCase());
|
||||
return `${normalized}-nativefier-${postFixHash}`;
|
||||
}
|
||||
|
||||
function changeAppPackageJsonName(appPath, name, url) {
|
||||
const packageJsonPath = path.join(appPath, '/package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath));
|
||||
packageJson.name = normalizeAppName(name, url);
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary directory and copies the './app folder' inside,
|
||||
* and adds a text file with the configuration for the single page app.
|
||||
*
|
||||
* @param {string} src
|
||||
* @param {string} dest
|
||||
* @param {{}} options
|
||||
* @param callback
|
||||
*/
|
||||
function buildApp(src, dest, options, callback) {
|
||||
const appArgs = selectAppArgs(options);
|
||||
|
||||
copy(src, dest, (error) => {
|
||||
if (error) {
|
||||
callback(`Error Copying temporary directory: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dest, '/nativefier.json'),
|
||||
JSON.stringify(appArgs),
|
||||
);
|
||||
|
||||
maybeCopyScripts(options.inject, dest)
|
||||
.catch((err) => {
|
||||
log.warn(err);
|
||||
})
|
||||
.then(() => {
|
||||
changeAppPackageJsonName(dest, appArgs.name, appArgs.targetUrl);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default buildApp;
|
96
src/build/buildIcon.ts
Normal file
96
src/build/buildIcon.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { isOSX } from '../helpers/helpers';
|
||||
import {
|
||||
convertToPng,
|
||||
convertToIco,
|
||||
convertToIcns,
|
||||
} from '../helpers/iconShellHelpers';
|
||||
import { AppOptions } from '../options/model';
|
||||
|
||||
function iconIsIco(iconPath: string): boolean {
|
||||
return path.extname(iconPath) === '.ico';
|
||||
}
|
||||
|
||||
function iconIsPng(iconPath: string): boolean {
|
||||
return path.extname(iconPath) === '.png';
|
||||
}
|
||||
|
||||
function iconIsIcns(iconPath: string): boolean {
|
||||
return path.extname(iconPath) === '.icns';
|
||||
}
|
||||
|
||||
/**
|
||||
* Will convert a `.png` icon to the appropriate arch format (if necessary),
|
||||
* and return adjusted options
|
||||
*/
|
||||
export async function convertIconIfNecessary(
|
||||
options: AppOptions,
|
||||
): Promise<void> {
|
||||
if (!options.packager.icon) {
|
||||
log.debug('Option "icon" not set, skipping icon conversion.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.packager.platform === 'win32') {
|
||||
if (iconIsIco(options.packager.icon)) {
|
||||
log.debug(
|
||||
'Building for Windows and icon is already a .ico, no conversion needed',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const iconPath = await convertToIco(options.packager.icon);
|
||||
options.packager.icon = iconPath;
|
||||
return;
|
||||
} catch (error) {
|
||||
log.warn('Failed to convert icon to .ico, skipping.', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.packager.platform === 'linux') {
|
||||
if (iconIsPng(options.packager.icon)) {
|
||||
log.debug(
|
||||
'Building for Linux and icon is already a .png, no conversion needed',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const iconPath = await convertToPng(options.packager.icon);
|
||||
options.packager.icon = iconPath;
|
||||
return;
|
||||
} catch (error) {
|
||||
log.warn('Failed to convert icon to .png, skipping.', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (iconIsIcns(options.packager.icon)) {
|
||||
log.debug(
|
||||
'Building for macOS and icon is already a .icns, no conversion needed',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOSX()) {
|
||||
log.warn(
|
||||
'Skipping icon conversion to .icns, conversion is only supported on macOS',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const iconPath = await convertToIcns(options.packager.icon);
|
||||
options.packager.icon = iconPath;
|
||||
return;
|
||||
} catch (error) {
|
||||
log.warn('Failed to convert icon to .icns, skipping.', error);
|
||||
options.packager.icon = undefined;
|
||||
return;
|
||||
}
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
import path from 'path';
|
||||
import packager from 'electron-packager';
|
||||
import tmp from 'tmp';
|
||||
import ncp from 'ncp';
|
||||
import async from 'async';
|
||||
import hasBinary from 'hasbin';
|
||||
import log from 'loglevel';
|
||||
import DishonestProgress from '../helpers/dishonestProgress';
|
||||
import optionsFactory from '../options/optionsMain';
|
||||
import iconBuild from './iconBuild';
|
||||
import helpers from '../helpers/helpers';
|
||||
import PackagerConsole from '../helpers/packagerConsole';
|
||||
import buildApp from './buildApp';
|
||||
|
||||
const copy = ncp.ncp;
|
||||
const { isWindows } = helpers;
|
||||
|
||||
/**
|
||||
* Checks the app path array to determine if the packaging was completed successfully
|
||||
* @param appPathArray Result from electron-packager
|
||||
* @returns {*}
|
||||
*/
|
||||
function getAppPath(appPathArray) {
|
||||
if (appPathArray.length === 0) {
|
||||
// directory already exists, --overwrite is not set
|
||||
// exit here
|
||||
return null;
|
||||
}
|
||||
|
||||
if (appPathArray.length > 1) {
|
||||
log.warn(
|
||||
'Warning: This should not be happening, packaged app path contains more than one element:',
|
||||
appPathArray,
|
||||
);
|
||||
}
|
||||
|
||||
return appPathArray[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `icon` parameter from options if building for Windows while not on Windows
|
||||
* and Wine is not installed
|
||||
* @param options
|
||||
*/
|
||||
function maybeNoIconOption(options) {
|
||||
const packageOptions = JSON.parse(JSON.stringify(options));
|
||||
if (options.platform === 'win32' && !isWindows()) {
|
||||
if (!hasBinary.sync('wine')) {
|
||||
log.warn(
|
||||
'Wine is required to set the icon for a Windows app when packaging on non-windows platforms',
|
||||
);
|
||||
packageOptions.icon = null;
|
||||
}
|
||||
}
|
||||
return packageOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* For windows and linux, we have to copy over the icon to the resources/app folder, which the
|
||||
* BrowserWindow is hard coded to read the icon from
|
||||
* @param {{}} options
|
||||
* @param {string} appPath
|
||||
* @param callback
|
||||
*/
|
||||
function maybeCopyIcons(options, appPath, callback) {
|
||||
if (!options.icon) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.platform === 'darwin' || options.platform === 'mas') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// windows & linux
|
||||
// put the icon file into the app
|
||||
const destIconPath = path.join(appPath, 'resources/app');
|
||||
const destFileName = `icon${path.extname(options.icon)}`;
|
||||
copy(options.icon, path.join(destIconPath, destFileName), (error) => {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes invalid parameters from options if building for Windows while not on Windows
|
||||
* and Wine is not installed
|
||||
* @param options
|
||||
*/
|
||||
function removeInvalidOptions(options, param) {
|
||||
const packageOptions = JSON.parse(JSON.stringify(options));
|
||||
if (options.platform === 'win32' && !isWindows()) {
|
||||
if (!hasBinary.sync('wine')) {
|
||||
log.warn(
|
||||
`Wine is required to use "${param}" option for a Windows app when packaging on non-windows platforms`,
|
||||
);
|
||||
packageOptions[param] = null;
|
||||
}
|
||||
}
|
||||
return packageOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `appCopyright` parameter from options if building for Windows while not on Windows
|
||||
* and Wine is not installed
|
||||
* @param options
|
||||
*/
|
||||
function maybeNoAppCopyrightOption(options) {
|
||||
return removeInvalidOptions(options, 'appCopyright');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `buildVersion` parameter from options if building for Windows while not on Windows
|
||||
* and Wine is not installed
|
||||
* @param options
|
||||
*/
|
||||
function maybeNoBuildVersionOption(options) {
|
||||
return removeInvalidOptions(options, 'buildVersion');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `appVersion` parameter from options if building for Windows while not on Windows
|
||||
* and Wine is not installed
|
||||
* @param options
|
||||
*/
|
||||
function maybeNoAppVersionOption(options) {
|
||||
return removeInvalidOptions(options, 'appVersion');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `versionString` parameter from options if building for Windows while not on Windows
|
||||
* and Wine is not installed
|
||||
* @param options
|
||||
*/
|
||||
function maybeNoVersionStringOption(options) {
|
||||
return removeInvalidOptions(options, 'versionString');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `win32metadata` parameter from options if building for Windows while not on Windows
|
||||
* and Wine is not installed
|
||||
* @param options
|
||||
*/
|
||||
function maybeNoWin32metadataOption(options) {
|
||||
return removeInvalidOptions(options, 'win32metadata');
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback buildAppCallback
|
||||
* @param error
|
||||
* @param {string} appPath
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{}} inpOptions
|
||||
* @param {buildAppCallback} callback
|
||||
*/
|
||||
function buildMain(inpOptions, callback) {
|
||||
const options = Object.assign({}, inpOptions);
|
||||
|
||||
// pre process app
|
||||
const tmpObj = tmp.dirSync({ mode: '0755', unsafeCleanup: true });
|
||||
const tmpPath = tmpObj.name;
|
||||
|
||||
// todo check if this is still needed on later version of packager
|
||||
const packagerConsole = new PackagerConsole();
|
||||
|
||||
const progress = new DishonestProgress(5);
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
(cb) => {
|
||||
progress.tick('inferring');
|
||||
optionsFactory(options)
|
||||
.then((result) => {
|
||||
cb(null, result);
|
||||
})
|
||||
.catch((error) => {
|
||||
cb(error);
|
||||
});
|
||||
},
|
||||
(opts, cb) => {
|
||||
progress.tick('copying');
|
||||
buildApp(opts.dir, tmpPath, opts, (error) => {
|
||||
if (error) {
|
||||
cb(error);
|
||||
return;
|
||||
}
|
||||
// Change the reference file for the Electron app to be the temporary path
|
||||
const newOptions = Object.assign({}, opts, {
|
||||
dir: tmpPath,
|
||||
});
|
||||
cb(null, newOptions);
|
||||
});
|
||||
},
|
||||
(opts, cb) => {
|
||||
progress.tick('icons');
|
||||
iconBuild(opts, (error, optionsWithIcon) => {
|
||||
cb(null, optionsWithIcon);
|
||||
});
|
||||
},
|
||||
(opts, cb) => {
|
||||
progress.tick('packaging');
|
||||
// maybe skip passing icon parameter to electron packager
|
||||
let packageOptions = maybeNoIconOption(opts);
|
||||
// maybe skip passing other parameters to electron packager
|
||||
packageOptions = maybeNoAppCopyrightOption(packageOptions);
|
||||
packageOptions = maybeNoAppVersionOption(packageOptions);
|
||||
packageOptions = maybeNoBuildVersionOption(packageOptions);
|
||||
packageOptions = maybeNoVersionStringOption(packageOptions);
|
||||
packageOptions = maybeNoWin32metadataOption(packageOptions);
|
||||
|
||||
packagerConsole.override();
|
||||
|
||||
packager(packageOptions)
|
||||
.then((appPathArray) => {
|
||||
packagerConsole.restore(); // restore console.error
|
||||
cb(null, opts, appPathArray); // options still contain the icon to waterfall
|
||||
})
|
||||
.catch((error) => {
|
||||
packagerConsole.restore(); // restore console.error
|
||||
cb(error, opts); // options still contain the icon to waterfall
|
||||
});
|
||||
},
|
||||
(opts, appPathArray, cb) => {
|
||||
progress.tick('finalizing');
|
||||
// somehow appPathArray is a 1 element array
|
||||
const appPath = getAppPath(appPathArray);
|
||||
if (!appPath) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
maybeCopyIcons(opts, appPath, (error) => {
|
||||
cb(error, appPath);
|
||||
});
|
||||
},
|
||||
],
|
||||
(error, appPath) => {
|
||||
packagerConsole.playback();
|
||||
callback(error, appPath);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default buildMain;
|
125
src/build/buildNativefierApp.ts
Normal file
125
src/build/buildNativefierApp.ts
Normal file
@ -0,0 +1,125 @@
|
||||
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 { isWindows, getTempDir, copyFileOrDir } from '../helpers/helpers';
|
||||
import { getOptions } from '../options/optionsMain';
|
||||
import { prepareElectronApp } from './prepareElectronApp';
|
||||
import { convertIconIfNecessary } from './buildIcon';
|
||||
import { AppOptions } from '../options/model';
|
||||
|
||||
const OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD = [
|
||||
'icon',
|
||||
'appCopyright',
|
||||
'appVersion',
|
||||
'buildVersion',
|
||||
'versionString',
|
||||
'win32metadata',
|
||||
];
|
||||
|
||||
/**
|
||||
* Checks the app path array to determine if packaging completed successfully
|
||||
*/
|
||||
function getAppPath(appPath: string | string[]): string {
|
||||
if (!Array.isArray(appPath)) {
|
||||
return appPath;
|
||||
}
|
||||
|
||||
if (appPath.length === 0) {
|
||||
return null; // directory already exists and `--overwrite` not set
|
||||
}
|
||||
|
||||
if (appPath.length > 1) {
|
||||
log.warn(
|
||||
'Warning: This should not be happening, packaged app path contains more than one element:',
|
||||
appPath,
|
||||
);
|
||||
}
|
||||
|
||||
return appPath[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* For Windows & Linux, we have to copy over the icon to the resources/app
|
||||
* folder, which the BrowserWindow is hard-coded to read the icon from
|
||||
*/
|
||||
async function copyIconsIfNecessary(
|
||||
options: AppOptions,
|
||||
appPath: string,
|
||||
): Promise<void> {
|
||||
log.debug('Copying icons if necessary');
|
||||
if (!options.packager.icon) {
|
||||
log.debug('No icon specified in options; aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
options.packager.platform === 'darwin' ||
|
||||
options.packager.platform === 'mas'
|
||||
) {
|
||||
log.debug('No copying necessary on macOS; aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
// windows & linux: put the icon file into the app
|
||||
const destAppPath = path.join(appPath, 'resources/app');
|
||||
const destFileName = `icon${path.extname(options.packager.icon)}`;
|
||||
const destIconPath = path.join(destAppPath, destFileName);
|
||||
|
||||
log.debug(`Copying icon ${options.packager.icon} to`, destIconPath);
|
||||
await copyFileOrDir(options.packager.icon, destIconPath);
|
||||
}
|
||||
|
||||
function trimUnprocessableOptions(options: AppOptions): void {
|
||||
if (
|
||||
options.packager.platform === 'win32' &&
|
||||
!isWindows() &&
|
||||
!hasbin.sync('wine')
|
||||
) {
|
||||
const optionsPresent = Object.entries(options)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD.includes(key) && !!value,
|
||||
)
|
||||
.map(([key]) => key);
|
||||
if (optionsPresent.length === 0) {
|
||||
return;
|
||||
}
|
||||
log.warn(
|
||||
`*Not* setting [${optionsPresent.join(', ')}], as couldn't find Wine.`,
|
||||
'Wine is required when packaging a Windows app under on non-Windows platforms.',
|
||||
);
|
||||
for (const keyToUnset of optionsPresent) {
|
||||
options[keyToUnset] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildNativefierApp(rawOptions: any): Promise<string> {
|
||||
log.info('Processing options...');
|
||||
const options = await getOptions(rawOptions);
|
||||
|
||||
log.info('\nPreparing Electron app...');
|
||||
const tmpPath = getTempDir('app', 0o755);
|
||||
await prepareElectronApp(options.packager.dir, tmpPath, options);
|
||||
|
||||
log.info('\nConverting icons...');
|
||||
options.packager.dir = tmpPath; // const optionsWithTmpPath = { ...options, dir: tmpPath };
|
||||
await convertIconIfNecessary(options);
|
||||
|
||||
log.info(
|
||||
"\nPackaging... This will take a few seconds, maybe minutes if the requested Electron isn't cached yet...",
|
||||
);
|
||||
trimUnprocessableOptions(options);
|
||||
electronGet.initializeProxy(); // https://github.com/electron/get#proxies
|
||||
const appPathArray = await electronPackager(options.packager);
|
||||
|
||||
log.info('\nFinalizing build...');
|
||||
const appPath = getAppPath(appPathArray);
|
||||
await copyIconsIfNecessary(options, appPath);
|
||||
|
||||
return appPath;
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
import path from 'path';
|
||||
import log from 'loglevel';
|
||||
import helpers from '../helpers/helpers';
|
||||
import iconShellHelpers from '../helpers/iconShellHelpers';
|
||||
|
||||
const { isOSX } = helpers;
|
||||
const { convertToPng, convertToIco, convertToIcns } = iconShellHelpers;
|
||||
|
||||
function iconIsIco(iconPath) {
|
||||
return path.extname(iconPath) === '.ico';
|
||||
}
|
||||
|
||||
function iconIsPng(iconPath) {
|
||||
return path.extname(iconPath) === '.png';
|
||||
}
|
||||
|
||||
function iconIsIcns(iconPath) {
|
||||
return path.extname(iconPath) === '.icns';
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback augmentIconsCallback
|
||||
* @param error
|
||||
* @param options
|
||||
*/
|
||||
|
||||
/**
|
||||
* Will check and convert a `.png` to `.icns` if necessary and augment
|
||||
* options.icon with the result
|
||||
*
|
||||
* @param inpOptions will need options.platform and options.icon
|
||||
* @param {augmentIconsCallback} callback
|
||||
*/
|
||||
function iconBuild(inpOptions, callback) {
|
||||
const options = Object.assign({}, inpOptions);
|
||||
const returnCallback = () => {
|
||||
callback(null, options);
|
||||
};
|
||||
|
||||
if (!options.icon) {
|
||||
returnCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.platform === 'win32') {
|
||||
if (iconIsIco(options.icon)) {
|
||||
returnCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
convertToIco(options.icon)
|
||||
.then((outPath) => {
|
||||
options.icon = outPath;
|
||||
returnCallback();
|
||||
})
|
||||
.catch((error) => {
|
||||
log.warn('Skipping icon conversion to .ico', error);
|
||||
returnCallback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.platform === 'linux') {
|
||||
if (iconIsPng(options.icon)) {
|
||||
returnCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
convertToPng(options.icon)
|
||||
.then((outPath) => {
|
||||
options.icon = outPath;
|
||||
returnCallback();
|
||||
})
|
||||
.catch((error) => {
|
||||
log.warn('Skipping icon conversion to .png', error);
|
||||
returnCallback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (iconIsIcns(options.icon)) {
|
||||
returnCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOSX()) {
|
||||
log.warn(
|
||||
'Skipping icon conversion to .icns, conversion is only supported on OSX',
|
||||
);
|
||||
returnCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
convertToIcns(options.icon)
|
||||
.then((outPath) => {
|
||||
options.icon = outPath;
|
||||
returnCallback();
|
||||
})
|
||||
.catch((error) => {
|
||||
log.warn('Skipping icon conversion to .icns', error);
|
||||
options.icon = undefined;
|
||||
returnCallback();
|
||||
});
|
||||
}
|
||||
|
||||
export default iconBuild;
|
157
src/build/prepareElectronApp.ts
Normal file
157
src/build/prepareElectronApp.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import * as fs from 'fs';
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { kebabCase } from 'lodash';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { copyFileOrDir } from '../helpers/helpers';
|
||||
import { AppOptions } from '../options/model';
|
||||
|
||||
const writeFileAsync = promisify(fs.writeFile);
|
||||
|
||||
/**
|
||||
* Only picks certain app args to pass to nativefier.json
|
||||
*/
|
||||
function pickElectronAppArgs(options: AppOptions): any {
|
||||
return {
|
||||
alwaysOnTop: options.nativefier.alwaysOnTop,
|
||||
appCopyright: options.packager.appCopyright,
|
||||
appVersion: options.packager.appVersion,
|
||||
backgroundColor: options.nativefier.backgroundColor,
|
||||
basicAuthPassword: options.nativefier.basicAuthPassword,
|
||||
basicAuthUsername: options.nativefier.basicAuthUsername,
|
||||
bounce: options.nativefier.bounce,
|
||||
browserwindowOptions: options.nativefier.browserwindowOptions,
|
||||
buildVersion: options.packager.buildVersion,
|
||||
clearCache: options.nativefier.clearCache,
|
||||
counter: options.nativefier.counter,
|
||||
crashReporter: options.nativefier.crashReporter,
|
||||
darwinDarkModeSupport: options.packager.darwinDarkModeSupport,
|
||||
disableContextMenu: options.nativefier.disableContextMenu,
|
||||
disableDevTools: options.nativefier.disableDevTools,
|
||||
disableGpu: options.nativefier.disableGpu,
|
||||
diskCacheSize: options.nativefier.diskCacheSize,
|
||||
enableEs3Apis: options.nativefier.enableEs3Apis,
|
||||
fastQuit: options.nativefier.fastQuit,
|
||||
fileDownloadOptions: options.nativefier.fileDownloadOptions,
|
||||
flashPluginDir: options.nativefier.flashPluginDir,
|
||||
fullScreen: options.nativefier.fullScreen,
|
||||
globalShortcuts: options.nativefier.globalShortcuts,
|
||||
height: options.nativefier.height,
|
||||
hideWindowFrame: options.nativefier.hideWindowFrame,
|
||||
ignoreCertificate: options.nativefier.ignoreCertificate,
|
||||
ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist,
|
||||
insecure: options.nativefier.insecure,
|
||||
internalUrls: options.nativefier.internalUrls,
|
||||
maxHeight: options.nativefier.maxHeight,
|
||||
maximize: options.nativefier.maximize,
|
||||
maxWidth: options.nativefier.maxWidth,
|
||||
minHeight: options.nativefier.minHeight,
|
||||
minWidth: options.nativefier.minWidth,
|
||||
name: options.packager.name,
|
||||
nativefierVersion: options.nativefier.nativefierVersion,
|
||||
processEnvs: options.nativefier.processEnvs,
|
||||
proxyRules: options.nativefier.proxyRules,
|
||||
showMenuBar: options.nativefier.showMenuBar,
|
||||
singleInstance: options.nativefier.singleInstance,
|
||||
targetUrl: options.packager.targetUrl,
|
||||
titleBarStyle: options.nativefier.titleBarStyle,
|
||||
tray: options.nativefier.tray,
|
||||
userAgent: options.nativefier.userAgent,
|
||||
versionString: options.nativefier.versionString,
|
||||
width: options.nativefier.width,
|
||||
win32metadata: options.packager.win32metadata,
|
||||
x: options.nativefier.x,
|
||||
y: options.nativefier.y,
|
||||
zoom: options.nativefier.zoom,
|
||||
};
|
||||
}
|
||||
|
||||
async function maybeCopyScripts(srcs: string[], dest: string): Promise<void> {
|
||||
if (!srcs || srcs.length === 0) {
|
||||
log.debug('No files to inject, skipping copy.');
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Copying ${srcs.length} files to inject in app.`);
|
||||
for (const src of srcs) {
|
||||
if (!fs.existsSync(src)) {
|
||||
throw new Error(
|
||||
`File ${src} not found. Note that Nativefier expects *local* files, not URLs.`,
|
||||
);
|
||||
}
|
||||
|
||||
let destFileName: string;
|
||||
if (path.extname(src) === '.js') {
|
||||
destFileName = 'inject.js';
|
||||
} else if (path.extname(src) === '.css') {
|
||||
destFileName = 'inject.css';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const destPath = path.join(dest, 'inject', destFileName);
|
||||
log.debug(`Copying injection file "${src}" to "${destPath}"`);
|
||||
await copyFileOrDir(src, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAppName(appName: string, url: string): string {
|
||||
// use a simple 3 byte random string to prevent collision
|
||||
const hash = crypto.createHash('md5');
|
||||
hash.update(url);
|
||||
const postFixHash = hash.digest('hex').substring(0, 6);
|
||||
const normalized = kebabCase(appName.toLowerCase());
|
||||
return `${normalized}-nativefier-${postFixHash}`;
|
||||
}
|
||||
|
||||
function changeAppPackageJsonName(
|
||||
appPath: string,
|
||||
name: string,
|
||||
url: string,
|
||||
): void {
|
||||
const packageJsonPath = path.join(appPath, '/package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString());
|
||||
const normalizedAppName = normalizeAppName(name, url);
|
||||
packageJson.name = normalizedAppName;
|
||||
log.debug(`Updating ${packageJsonPath} 'name' field to ${normalizedAppName}`);
|
||||
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary directory, copies the './app folder' inside,
|
||||
* and adds a text file with the app configuration.
|
||||
*/
|
||||
export async function prepareElectronApp(
|
||||
src: string,
|
||||
dest: string,
|
||||
options: AppOptions,
|
||||
): Promise<void> {
|
||||
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}`;
|
||||
}
|
||||
|
||||
const appJsonPath = path.join(dest, '/nativefier.json');
|
||||
log.debug(`Writing app config to ${appJsonPath}`);
|
||||
await writeFileAsync(
|
||||
appJsonPath,
|
||||
JSON.stringify(pickElectronAppArgs(options)),
|
||||
);
|
||||
|
||||
try {
|
||||
await maybeCopyScripts(options.nativefier.inject, dest);
|
||||
} catch (err) {
|
||||
log.error('Error copying injection files.', err);
|
||||
}
|
||||
changeAppPackageJsonName(
|
||||
dest,
|
||||
options.packager.name,
|
||||
options.packager.targetUrl,
|
||||
);
|
||||
}
|
@ -1,19 +1,22 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
#!/usr/bin/env node
|
||||
import 'source-map-support/register';
|
||||
import program from 'commander';
|
||||
import nativefier from './index';
|
||||
|
||||
const dns = require('dns');
|
||||
const log = require('loglevel');
|
||||
const packageJson = require('./../package');
|
||||
import * as commander from 'commander';
|
||||
import * as dns from 'dns';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
function collect(val, memo) {
|
||||
import { buildNativefierApp } from './main';
|
||||
|
||||
// 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
|
||||
const packageJson = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
|
||||
function collect(val: any, memo: any[]): any[] {
|
||||
memo.push(val);
|
||||
return memo;
|
||||
}
|
||||
|
||||
function parseMaybeBoolString(val) {
|
||||
function parseBooleanOrString(val: string): boolean | string {
|
||||
switch (val) {
|
||||
case 'true':
|
||||
return true;
|
||||
@ -24,19 +27,19 @@ function parseMaybeBoolString(val) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseJson(val) {
|
||||
function parseJson(val: string): any {
|
||||
if (!val) return {};
|
||||
return JSON.parse(val);
|
||||
}
|
||||
|
||||
function getProcessEnvs(val) {
|
||||
if (!val) return {};
|
||||
const pEnv = {};
|
||||
pEnv.processEnvs = parseJson(val);
|
||||
return pEnv;
|
||||
function getProcessEnvs(val: string): any {
|
||||
if (!val) {
|
||||
return {};
|
||||
}
|
||||
return { processEnvs: parseJson(val) };
|
||||
}
|
||||
|
||||
function checkInternet() {
|
||||
function checkInternet(): void {
|
||||
dns.lookup('npmjs.com', (err) => {
|
||||
if (err && err.code === 'ENOTFOUND') {
|
||||
log.warn(
|
||||
@ -63,31 +66,36 @@ if (require.main === module) {
|
||||
sanitizedArgs.push(arg);
|
||||
});
|
||||
|
||||
program
|
||||
const positionalOptions = {
|
||||
targetUrl: '',
|
||||
out: '',
|
||||
};
|
||||
commander
|
||||
.name('nativefier')
|
||||
.version(packageJson.version, '-v, --version')
|
||||
.arguments('<targetUrl> [dest]')
|
||||
.action((targetUrl, appDir) => {
|
||||
program.targetUrl = targetUrl;
|
||||
program.out = appDir;
|
||||
.action((url, outputDirectory) => {
|
||||
positionalOptions.targetUrl = url;
|
||||
positionalOptions.out = outputDirectory;
|
||||
})
|
||||
.option('-n, --name <value>', 'app name')
|
||||
.option('-p, --platform <value>', "'osx', 'mas', 'linux' or 'windows'")
|
||||
.option('-p, --platform <value>', "'mac', 'mas', 'linux' or 'windows'")
|
||||
.option('-a, --arch <value>', "'ia32' or 'x64' or 'armv7l'")
|
||||
.option(
|
||||
'--app-version <value>',
|
||||
'The release version of the application. Maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on OS X.',
|
||||
'(macOS, windows only) the version of the app. Maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on macOS.',
|
||||
)
|
||||
.option(
|
||||
'--build-version <value>',
|
||||
'The build version of the application. Maps to the `FileVersion` metadata property on Windows, and `CFBundleVersion` on OS X.',
|
||||
'(macOS, windows only) The build version of the app. Maps to `FileVersion` metadata property on Windows, and `CFBundleVersion` on macOS',
|
||||
)
|
||||
.option(
|
||||
'--app-copyright <value>',
|
||||
'The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata property on Windows, and `NSHumanReadableCopyright` on OS X',
|
||||
'(macOS, windows only) a human-readable copyright line for the app. Maps to `LegalCopyright` metadata property on Windows, and `NSHumanReadableCopyright` on macOS',
|
||||
)
|
||||
.option(
|
||||
'--win32metadata <json-string>',
|
||||
'a JSON string of key/value pairs of application metadata (ProductName, InternalName, FileDescription) to embed into the executable (Windows only).',
|
||||
'(windows only) a JSON string of key/value pairs (ProductName, InternalName, FileDescription) to embed as executable metadata',
|
||||
parseJson,
|
||||
)
|
||||
.option(
|
||||
@ -96,19 +104,19 @@ if (require.main === module) {
|
||||
)
|
||||
.option(
|
||||
'--no-overwrite',
|
||||
'do not override output directory if it already exists, defaults to false',
|
||||
'do not override output directory if it already exists; defaults to false',
|
||||
)
|
||||
.option(
|
||||
'-c, --conceal',
|
||||
'packages the source code within your app into an archive, defaults to false, see https://electronjs.org/docs/tutorial/application-packaging',
|
||||
'packages the app source code into an asar archive; defaults to false',
|
||||
)
|
||||
.option(
|
||||
'--counter',
|
||||
'if the target app should use a persistent counter badge in the dock (macOS only), defaults to false',
|
||||
'(macOS only) set a dock count badge, determined by looking for a number in the window title; defaults to false',
|
||||
)
|
||||
.option(
|
||||
'--bounce',
|
||||
'if the the dock icon should bounce when counter increases (macOS only), defaults to false',
|
||||
'(macOS only) make he dock icon bounce when the counter increases; defaults to false',
|
||||
)
|
||||
.option(
|
||||
'-i, --icon <value>',
|
||||
@ -116,61 +124,61 @@ if (require.main === module) {
|
||||
)
|
||||
.option(
|
||||
'--width <value>',
|
||||
'set window default width, defaults to 1280px',
|
||||
'set window default width; defaults to 1280px',
|
||||
parseInt,
|
||||
)
|
||||
.option(
|
||||
'--height <value>',
|
||||
'set window default height, defaults to 800px',
|
||||
'set window default height; defaults to 800px',
|
||||
parseInt,
|
||||
)
|
||||
.option(
|
||||
'--min-width <value>',
|
||||
'set window minimum width, defaults to 0px',
|
||||
'set window minimum width; defaults to 0px',
|
||||
parseInt,
|
||||
)
|
||||
.option(
|
||||
'--min-height <value>',
|
||||
'set window minimum height, defaults to 0px',
|
||||
'set window minimum height; defaults to 0px',
|
||||
parseInt,
|
||||
)
|
||||
.option(
|
||||
'--max-width <value>',
|
||||
'set window maximum width, default is no limit',
|
||||
'set window maximum width; default is unlimited',
|
||||
parseInt,
|
||||
)
|
||||
.option(
|
||||
'--max-height <value>',
|
||||
'set window maximum height, default is no limit',
|
||||
'set window maximum height; default is unlimited',
|
||||
parseInt,
|
||||
)
|
||||
.option('--x <value>', 'set window x location', parseInt)
|
||||
.option('--y <value>', 'set window y location', parseInt)
|
||||
.option('-m, --show-menu-bar', 'set menu bar visible, defaults to false')
|
||||
.option('-m, --show-menu-bar', 'set menu bar visible; defaults to false')
|
||||
.option(
|
||||
'-f, --fast-quit',
|
||||
'quit app after window close (macOS only), defaults to false',
|
||||
'(macOS only) quit app on window close; defaults to false',
|
||||
)
|
||||
.option('-u, --user-agent <value>', 'set the user agent string for the app')
|
||||
.option('-u, --user-agent <value>', 'set the app user agent string')
|
||||
.option(
|
||||
'--honest',
|
||||
'prevent the nativefied app from changing the user agent string to masquerade as a regular chrome browser',
|
||||
'prevent the normal changing of the user agent string to appear as a regular Chrome browser',
|
||||
)
|
||||
.option('--ignore-certificate', 'ignore certificate related errors')
|
||||
.option('--ignore-certificate', 'ignore certificate-related errors')
|
||||
.option('--disable-gpu', 'disable hardware acceleration')
|
||||
.option(
|
||||
'--ignore-gpu-blacklist',
|
||||
'allow WebGl apps to work on non supported graphics cards',
|
||||
'force WebGL apps to work on unsupported GPUs',
|
||||
)
|
||||
.option('--enable-es3-apis', 'force activation of WebGl 2.0')
|
||||
.option('--enable-es3-apis', 'force activation of WebGL 2.0')
|
||||
.option(
|
||||
'--insecure',
|
||||
'enable loading of insecure content, defaults to false',
|
||||
'enable loading of insecure content; defaults to false',
|
||||
)
|
||||
.option('--flash', 'if flash should be enabled')
|
||||
.option('--flash', 'enables Adobe Flash; defaults to false')
|
||||
.option(
|
||||
'--flash-path <value>',
|
||||
'path to Chrome flash plugin, find it in `Chrome://plugins`',
|
||||
'path to Chrome flash plugin; find it in `chrome://plugins`',
|
||||
)
|
||||
.option(
|
||||
'--disk-cache-size <value>',
|
||||
@ -178,31 +186,31 @@ if (require.main === module) {
|
||||
)
|
||||
.option(
|
||||
'--inject <value>',
|
||||
'path to a CSS/JS file to be injected',
|
||||
'path to a CSS/JS file to be injected. Pass multiple times to inject multiple files.',
|
||||
collect,
|
||||
[],
|
||||
)
|
||||
.option(
|
||||
'--full-screen',
|
||||
'if the app should always be started in full screen',
|
||||
)
|
||||
.option('--maximize', 'if the app should always be started maximized')
|
||||
.option('--full-screen', 'always start the app full screen')
|
||||
.option('--maximize', 'always start the app maximized')
|
||||
.option('--hide-window-frame', 'disable window frame and controls')
|
||||
.option('--verbose', 'if verbose logs should be displayed')
|
||||
.option('--disable-context-menu', 'disable the context menu')
|
||||
.option('--disable-dev-tools', 'disable developer tools')
|
||||
.option('--verbose', 'enable verbose/debug/troubleshooting logs')
|
||||
.option('--disable-context-menu', 'disable the context menu (right click)')
|
||||
.option(
|
||||
'--disable-dev-tools',
|
||||
'disable developer tools (Ctrl+Shift+I / F12)',
|
||||
)
|
||||
.option(
|
||||
'--zoom <value>',
|
||||
'default zoom factor to use when the app is opened, defaults to 1.0',
|
||||
'default zoom factor to use when the app is opened; defaults to 1.0',
|
||||
parseFloat,
|
||||
)
|
||||
.option(
|
||||
'--internal-urls <value>',
|
||||
'regular expression of URLs to consider "internal"; all other URLs will be opened in an external browser. (default: URLs on same second-level domain as app)',
|
||||
'regex of URLs to consider "internal"; all other URLs will be opened in an external browser. Default: URLs on same second-level domain as app',
|
||||
)
|
||||
.option(
|
||||
'--proxy-rules <value>',
|
||||
'proxy rules. See https://electronjs.org/docs/api/session?q=proxy#sessetproxyconfig-callback',
|
||||
'proxy rules; see https://www.electronjs.org/docs/api/session#sessetproxyconfig',
|
||||
)
|
||||
.option(
|
||||
'--crash-reporter <value>',
|
||||
@ -218,7 +226,7 @@ if (require.main === module) {
|
||||
)
|
||||
.option(
|
||||
'--processEnvs <json-string>',
|
||||
'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened.',
|
||||
'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened',
|
||||
getProcessEnvs,
|
||||
)
|
||||
.option(
|
||||
@ -228,19 +236,19 @@ if (require.main === module) {
|
||||
)
|
||||
.option(
|
||||
'--tray [start-in-tray]',
|
||||
"Allow app to stay in system tray. If 'start-in-tray' is given as argument, don't show main window on first start",
|
||||
parseMaybeBoolString,
|
||||
"Allow app to stay in system tray. If 'start-in-tray' is set as argument, don't show main window on first start",
|
||||
parseBooleanOrString,
|
||||
)
|
||||
.option('--basic-auth-username <value>', 'basic http(s) auth username')
|
||||
.option('--basic-auth-password <value>', 'basic http(s) auth password')
|
||||
.option('--always-on-top', 'enable always on top window')
|
||||
.option(
|
||||
'--title-bar-style <value>',
|
||||
"(macOS only) set title bar style ('hidden', 'hiddenInset'). Consider injecting custom CSS (via --inject) for better integration.",
|
||||
"(macOS only) set title bar style ('hidden', 'hiddenInset'). Consider injecting custom CSS (via --inject) for better integration",
|
||||
)
|
||||
.option(
|
||||
'--global-shortcuts <value>',
|
||||
'JSON file with global shortcut configuration. See https://github.com/jiahaog/nativefier/blob/master/docs/api.md#global-shortcuts',
|
||||
'JSON file defining global shortcuts. See https://github.com/jiahaog/nativefier/blob/master/docs/api.md#global-shortcuts',
|
||||
)
|
||||
.option(
|
||||
'--browserwindow-options <json-string>',
|
||||
@ -249,7 +257,7 @@ if (require.main === module) {
|
||||
)
|
||||
.option(
|
||||
'--background-color <value>',
|
||||
"Sets the background color (for seamless experience while the app is loading). Example value: '#2e2c29'",
|
||||
"sets the app background color, for better integration while the app is loading. Example value: '#2e2c29'",
|
||||
)
|
||||
.option(
|
||||
'--darwin-dark-mode-support',
|
||||
@ -258,19 +266,19 @@ if (require.main === module) {
|
||||
.parse(sanitizedArgs);
|
||||
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.help();
|
||||
commander.help();
|
||||
}
|
||||
checkInternet();
|
||||
nativefier(program, (error, appPath) => {
|
||||
if (error) {
|
||||
log.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const options = { ...positionalOptions, ...commander.opts() };
|
||||
buildNativefierApp(options)
|
||||
.then((appPath) => {
|
||||
if (!appPath) {
|
||||
// app exists and --overwrite is not passed
|
||||
log.info(`App *not* built to ${appPath}`);
|
||||
return;
|
||||
}
|
||||
log.info(`App built to ${appPath}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error('Error during build. Run with --verbose for details.', error);
|
||||
});
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
export const DEFAULT_APP_NAME = 'APP';
|
||||
export const ELECTRON_VERSION = '5.0.13';
|
||||
export const PLACEHOLDER_APP_DIR = path.join(__dirname, './../', 'app');
|
13
src/constants.ts
Normal file
13
src/constants.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as path from 'path';
|
||||
|
||||
export const DEFAULT_APP_NAME = 'APP';
|
||||
|
||||
// Update both together
|
||||
export const DEFAULT_ELECTRON_VERSION = '8.1.1';
|
||||
export const DEFAULT_CHROME_VERSION = '80.0.3987.141';
|
||||
|
||||
export const ELECTRON_MAJOR_VERSION = parseInt(
|
||||
DEFAULT_ELECTRON_VERSION.split('.')[0],
|
||||
10,
|
||||
);
|
||||
export const PLACEHOLDER_APP_DIR = path.join(__dirname, './../', 'app');
|
@ -1,65 +0,0 @@
|
||||
import shell from 'shelljs';
|
||||
import path from 'path';
|
||||
import tmp from 'tmp';
|
||||
import helpers from './helpers';
|
||||
|
||||
const { isOSX } = helpers;
|
||||
tmp.setGracefulCleanup();
|
||||
|
||||
const PNG_TO_ICNS_BIN_PATH = path.join(__dirname, '../..', 'bin/convertToIcns');
|
||||
|
||||
/**
|
||||
* @callback pngToIcnsCallback
|
||||
* @param error
|
||||
* @param {string} icnsDest If error, will return the original png src
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} pngSrc
|
||||
* @param {string} icnsDest
|
||||
* @param {pngToIcnsCallback} callback
|
||||
*/
|
||||
function convertToIcns(pngSrc, icnsDest, callback) {
|
||||
if (!isOSX()) {
|
||||
callback('OSX is required to convert .png to .icns icon', pngSrc);
|
||||
return;
|
||||
}
|
||||
|
||||
shell.exec(
|
||||
`"${PNG_TO_ICNS_BIN_PATH}" "${pngSrc}" "${icnsDest}"`,
|
||||
{ silent: true },
|
||||
(exitCode, stdOut, stdError) => {
|
||||
if (stdOut.includes('icon.iconset:error') || exitCode) {
|
||||
if (exitCode) {
|
||||
callback(
|
||||
{
|
||||
stdOut,
|
||||
stdError,
|
||||
},
|
||||
pngSrc,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(stdOut, pngSrc);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, icnsDest);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the png to a temporary directory which will be cleaned up on process exit
|
||||
* @param {string} pngSrc
|
||||
* @param {pngToIcnsCallback} callback
|
||||
*/
|
||||
function convertToIcnsTmp(pngSrc, callback) {
|
||||
const tempIconDirObj = tmp.dirSync({ unsafeCleanup: true });
|
||||
const tempIconDirPath = tempIconDirObj.name;
|
||||
convertToIcns(pngSrc, `${tempIconDirPath}/icon.icns`, callback);
|
||||
}
|
||||
|
||||
export default convertToIcnsTmp;
|
@ -1,40 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import convertToIcns from './convertToIcns';
|
||||
|
||||
// Prerequisite for test: to use OSX with sips, iconutil and imagemagick convert
|
||||
|
||||
function testConvertPng(pngName) {
|
||||
if (os.platform() !== 'darwin') {
|
||||
// Skip png conversion tests, OSX is required
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
convertToIcns(
|
||||
path.join(__dirname, '../../', 'test-resources', pngName),
|
||||
(error, icnsPath) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = fs.statSync(icnsPath);
|
||||
|
||||
expect(stat.isFile()).toBe(true);
|
||||
resolve();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe('Get Icon Module', () => {
|
||||
test('Can convert a rgb png to icns', async () => {
|
||||
await testConvertPng('iconSample.png');
|
||||
});
|
||||
|
||||
test('Can convert a grey png to icns', async () => {
|
||||
await testConvertPng('iconSampleGrey.png');
|
||||
});
|
||||
});
|
@ -1,70 +0,0 @@
|
||||
import ProgressBar from 'progress';
|
||||
|
||||
class DishonestProgress {
|
||||
constructor(total) {
|
||||
this.tickParts = total * 10;
|
||||
|
||||
this.bar = new ProgressBar(' :task [:bar] :percent', {
|
||||
complete: '=',
|
||||
incomplete: ' ',
|
||||
total: total * this.tickParts,
|
||||
width: 50,
|
||||
clear: true,
|
||||
});
|
||||
|
||||
this.tickingPrevious = {
|
||||
message: '',
|
||||
remainder: 0,
|
||||
interval: null,
|
||||
};
|
||||
}
|
||||
|
||||
tick(message) {
|
||||
const {
|
||||
remainder: prevRemainder,
|
||||
message: prevMessage,
|
||||
interval: prevInterval,
|
||||
} = this.tickingPrevious;
|
||||
|
||||
if (prevRemainder) {
|
||||
this.bar.tick(prevRemainder, {
|
||||
task: prevMessage,
|
||||
});
|
||||
clearInterval(prevInterval);
|
||||
}
|
||||
|
||||
const realRemainder = this.bar.total - this.bar.curr;
|
||||
if (realRemainder === this.tickParts) {
|
||||
this.bar.tick(this.tickParts, {
|
||||
task: message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.bar.tick({
|
||||
task: message,
|
||||
});
|
||||
|
||||
this.tickingPrevious = {
|
||||
message,
|
||||
remainder: this.tickParts,
|
||||
interval: null,
|
||||
};
|
||||
|
||||
this.tickingPrevious.remainder -= 1;
|
||||
|
||||
this.tickingPrevious.interval = setInterval(() => {
|
||||
if (this.tickingPrevious.remainder === 1) {
|
||||
clearInterval(this.tickingPrevious.interval);
|
||||
return;
|
||||
}
|
||||
|
||||
this.bar.tick({
|
||||
task: message,
|
||||
});
|
||||
this.tickingPrevious.remainder -= 1;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
export default DishonestProgress;
|
@ -1,107 +0,0 @@
|
||||
import os from 'os';
|
||||
import axios from 'axios';
|
||||
import hasBinary from 'hasbin';
|
||||
import path from 'path';
|
||||
|
||||
function isOSX() {
|
||||
return os.platform() === 'darwin';
|
||||
}
|
||||
|
||||
function isWindows() {
|
||||
return os.platform() === 'win32';
|
||||
}
|
||||
|
||||
function downloadFile(fileUrl) {
|
||||
return axios
|
||||
.get(fileUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.data) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
data: response.data,
|
||||
ext: path.extname(fileUrl),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function allowedIconFormats(platform) {
|
||||
const hasIdentify = hasBinary.sync('identify');
|
||||
const hasConvert = hasBinary.sync('convert');
|
||||
const hasIconUtil = hasBinary.sync('iconutil');
|
||||
|
||||
const pngToIcns = hasConvert && hasIconUtil;
|
||||
const pngToIco = hasConvert;
|
||||
const icoToIcns = pngToIcns && hasIdentify;
|
||||
const icoToPng = hasConvert;
|
||||
|
||||
// todo scripts for the following
|
||||
const icnsToPng = false;
|
||||
const icnsToIco = false;
|
||||
|
||||
const formats = [];
|
||||
|
||||
// todo shell scripting is not supported on windows, temporary override
|
||||
if (isWindows()) {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
formats.push('.icns');
|
||||
break;
|
||||
case 'linux':
|
||||
formats.push('.png');
|
||||
break;
|
||||
case 'win32':
|
||||
formats.push('.ico');
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`function allowedIconFormats error: Unknown platform ${platform}`,
|
||||
);
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
formats.push('.icns');
|
||||
if (pngToIcns) {
|
||||
formats.push('.png');
|
||||
}
|
||||
if (icoToIcns) {
|
||||
formats.push('.ico');
|
||||
}
|
||||
break;
|
||||
case 'linux':
|
||||
formats.push('.png');
|
||||
if (icoToPng) {
|
||||
formats.push('.ico');
|
||||
}
|
||||
if (icnsToPng) {
|
||||
formats.push('.icns');
|
||||
}
|
||||
break;
|
||||
case 'win32':
|
||||
formats.push('.ico');
|
||||
if (pngToIco) {
|
||||
formats.push('.png');
|
||||
}
|
||||
if (icnsToIco) {
|
||||
formats.push('.icns');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`function allowedIconFormats error: Unknown platform ${platform}`,
|
||||
);
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
|
||||
export default {
|
||||
isOSX,
|
||||
isWindows,
|
||||
downloadFile,
|
||||
allowedIconFormats,
|
||||
};
|
141
src/helpers/helpers.ts
Normal file
141
src/helpers/helpers.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import axios from 'axios';
|
||||
import * as hasbin from 'hasbin';
|
||||
import { ncp } from 'ncp';
|
||||
import * as log from 'loglevel';
|
||||
import * as tmp from 'tmp';
|
||||
tmp.setGracefulCleanup(); // cleanup temp dirs even when an uncaught exception occurs
|
||||
|
||||
const now = new Date();
|
||||
const TMP_TIME = `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`;
|
||||
|
||||
type DownloadResult = {
|
||||
data: Buffer;
|
||||
ext: string;
|
||||
};
|
||||
|
||||
export function isOSX(): boolean {
|
||||
return os.platform() === 'darwin';
|
||||
}
|
||||
|
||||
export function isWindows(): boolean {
|
||||
return os.platform() === 'win32';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temp directory with a debug-friendly name, and return its path.
|
||||
* Will be automatically deleted on exit.
|
||||
*/
|
||||
export function getTempDir(prefix: string, mode?: number): string {
|
||||
return tmp.dirSync({
|
||||
mode,
|
||||
unsafeCleanup: true, // recursively remove tmp dir on exit, even if not empty.
|
||||
prefix: `nativefier-${TMP_TIME}-${prefix}-`,
|
||||
}).name;
|
||||
}
|
||||
|
||||
export async function copyFileOrDir(
|
||||
sourceFileOrDir: string,
|
||||
dest: string,
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ncp(sourceFileOrDir, dest, (error: any) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadFile(fileUrl: string): Promise<DownloadResult> {
|
||||
log.debug(`Downloading ${fileUrl}`);
|
||||
return axios
|
||||
.get(fileUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.data) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
data: response.data,
|
||||
ext: path.extname(fileUrl),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getAllowedIconFormats(platform: string): string[] {
|
||||
const hasIdentify = hasbin.sync('identify');
|
||||
const hasConvert = hasbin.sync('convert');
|
||||
const hasIconUtil = hasbin.sync('iconutil');
|
||||
|
||||
const pngToIcns = hasConvert && hasIconUtil;
|
||||
const pngToIco = hasConvert;
|
||||
const icoToIcns = pngToIcns && hasIdentify;
|
||||
const icoToPng = hasConvert;
|
||||
|
||||
// Unsupported
|
||||
const icnsToPng = false;
|
||||
const icnsToIco = false;
|
||||
|
||||
const formats = [];
|
||||
|
||||
// Shell scripting is not supported on windows, temporary override
|
||||
if (isWindows()) {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
formats.push('.icns');
|
||||
break;
|
||||
case 'linux':
|
||||
formats.push('.png');
|
||||
break;
|
||||
case 'win32':
|
||||
formats.push('.ico');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown platform ${platform}`);
|
||||
}
|
||||
log.debug(
|
||||
`Allowed icon formats when building for ${platform} (limited on Windows):`,
|
||||
formats,
|
||||
);
|
||||
return formats;
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
formats.push('.icns');
|
||||
if (pngToIcns) {
|
||||
formats.push('.png');
|
||||
}
|
||||
if (icoToIcns) {
|
||||
formats.push('.ico');
|
||||
}
|
||||
break;
|
||||
case 'linux':
|
||||
formats.push('.png');
|
||||
if (icoToPng) {
|
||||
formats.push('.ico');
|
||||
}
|
||||
if (icnsToPng) {
|
||||
formats.push('.icns');
|
||||
}
|
||||
break;
|
||||
case 'win32':
|
||||
formats.push('.ico');
|
||||
if (pngToIco) {
|
||||
formats.push('.png');
|
||||
}
|
||||
if (icnsToIco) {
|
||||
formats.push('.icns');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown platform ${platform}`);
|
||||
}
|
||||
log.debug(`Allowed icon formats when building for ${platform}:`, formats);
|
||||
return formats;
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import shell from 'shelljs';
|
||||
import path from 'path';
|
||||
import tmp from 'tmp';
|
||||
import helpers from './helpers';
|
||||
|
||||
const { isWindows, isOSX } = helpers;
|
||||
|
||||
tmp.setGracefulCleanup();
|
||||
|
||||
const SCRIPT_PATHS = {
|
||||
singleIco: path.join(__dirname, '../..', 'bin/singleIco'),
|
||||
convertToPng: path.join(__dirname, '../..', 'bin/convertToPng'),
|
||||
convertToIco: path.join(__dirname, '../..', 'bin/convertToIco'),
|
||||
convertToIcns: path.join(__dirname, '../..', 'bin/convertToIcns'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a shell script with the form "./pathToScript param1 param2"
|
||||
* @param {string} shellScriptPath
|
||||
* @param {string} icoSrc input .ico
|
||||
* @param {string} dest has to be a .ico path
|
||||
*/
|
||||
function iconShellHelper(shellScriptPath, icoSrc, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isWindows()) {
|
||||
reject(new Error('OSX or Linux is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
shell.exec(
|
||||
`"${shellScriptPath}" "${icoSrc}" "${dest}"`,
|
||||
{ silent: true },
|
||||
(exitCode, stdOut, stdError) => {
|
||||
if (exitCode) {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject({
|
||||
stdOut,
|
||||
stdError,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(dest);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getTmpDirPath() {
|
||||
const tempIconDirObj = tmp.dirSync({ unsafeCleanup: true });
|
||||
return tempIconDirObj.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the ico to a temporary directory which will be cleaned up on process exit
|
||||
* @param {string} icoSrc path to a .ico file
|
||||
* @return {Promise}
|
||||
*/
|
||||
|
||||
function singleIco(icoSrc) {
|
||||
return iconShellHelper(
|
||||
SCRIPT_PATHS.singleIco,
|
||||
icoSrc,
|
||||
`${getTmpDirPath()}/icon.ico`,
|
||||
);
|
||||
}
|
||||
|
||||
function convertToPng(icoSrc) {
|
||||
return iconShellHelper(
|
||||
SCRIPT_PATHS.convertToPng,
|
||||
icoSrc,
|
||||
`${getTmpDirPath()}/icon.png`,
|
||||
);
|
||||
}
|
||||
|
||||
function convertToIco(icoSrc) {
|
||||
return iconShellHelper(
|
||||
SCRIPT_PATHS.convertToIco,
|
||||
icoSrc,
|
||||
`${getTmpDirPath()}/icon.ico`,
|
||||
);
|
||||
}
|
||||
|
||||
function convertToIcns(icoSrc) {
|
||||
if (!isOSX()) {
|
||||
return new Promise((resolve, reject) =>
|
||||
reject(new Error('OSX is required to convert to a .icns icon')),
|
||||
);
|
||||
}
|
||||
return iconShellHelper(
|
||||
SCRIPT_PATHS.convertToIcns,
|
||||
icoSrc,
|
||||
`${getTmpDirPath()}/icon.icns`,
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
singleIco,
|
||||
convertToPng,
|
||||
convertToIco,
|
||||
convertToIcns,
|
||||
};
|
89
src/helpers/iconShellHelpers.ts
Normal file
89
src/helpers/iconShellHelpers.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import * as shell from 'shelljs';
|
||||
|
||||
import { isWindows, isOSX, getTempDir } from './helpers';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
const SCRIPT_PATHS = {
|
||||
singleIco: path.join(__dirname, '../..', 'icon-scripts/singleIco'),
|
||||
convertToPng: path.join(__dirname, '../..', 'icon-scripts/convertToPng'),
|
||||
convertToIco: path.join(__dirname, '../..', 'icon-scripts/convertToIco'),
|
||||
convertToIcns: path.join(__dirname, '../..', 'icon-scripts/convertToIcns'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a shell script with the form "./pathToScript param1 param2"
|
||||
*/
|
||||
async function iconShellHelper(
|
||||
shellScriptPath: string,
|
||||
icoSource: string,
|
||||
icoDestination: string,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isWindows()) {
|
||||
reject(
|
||||
new Error(
|
||||
'Icon conversion only supported on macOS or Linux. ' +
|
||||
'If building for Windows, download/create a .ico and pass it with --icon favicon.ico . ' +
|
||||
'If building for macOS/Linux, do it from macOS/Linux',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const shellCommand = `"${shellScriptPath}" "${icoSource}" "${icoDestination}"`;
|
||||
log.debug(
|
||||
`Converting icon ${icoSource} to ${icoDestination}.`,
|
||||
`Calling: ${shellCommand}`,
|
||||
);
|
||||
shell.exec(shellCommand, { silent: true }, (exitCode, stdOut, stdError) => {
|
||||
if (exitCode) {
|
||||
reject({
|
||||
stdOut,
|
||||
stdError,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Conversion succeeded and produced icon at ${icoDestination}`);
|
||||
resolve(icoDestination);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function singleIco(icoSrc: string): Promise<string> {
|
||||
return iconShellHelper(
|
||||
SCRIPT_PATHS.singleIco,
|
||||
icoSrc,
|
||||
`${getTempDir('iconconv')}/icon.ico`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function convertToPng(icoSrc: string): Promise<string> {
|
||||
return iconShellHelper(
|
||||
SCRIPT_PATHS.convertToPng,
|
||||
icoSrc,
|
||||
`${getTempDir('iconconv')}/icon.png`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function convertToIco(icoSrc: string): Promise<string> {
|
||||
return iconShellHelper(
|
||||
SCRIPT_PATHS.convertToIco,
|
||||
icoSrc,
|
||||
`${getTempDir('iconconv')}/icon.ico`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function convertToIcns(icoSrc: string): Promise<string> {
|
||||
if (!isOSX()) {
|
||||
throw new Error('macOS is required to convert to a .icns icon');
|
||||
}
|
||||
|
||||
return iconShellHelper(
|
||||
SCRIPT_PATHS.convertToIcns,
|
||||
icoSrc,
|
||||
`${getTempDir('iconconv')}/icon.icns`,
|
||||
);
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
// TODO: remove this file and use quiet mode of new version of electron packager
|
||||
const log = require('loglevel');
|
||||
|
||||
class PackagerConsole {
|
||||
constructor() {
|
||||
this.logs = [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
_log(...messages) {
|
||||
this.logs.push(...messages);
|
||||
}
|
||||
|
||||
override() {
|
||||
this.consoleError = log.error;
|
||||
|
||||
// need to bind because somehow when _log() is called this refers to console
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
log.error = this._log.bind(this);
|
||||
}
|
||||
|
||||
restore() {
|
||||
log.error = this.consoleError;
|
||||
}
|
||||
|
||||
playback() {
|
||||
log.log(this.logs.join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
export default PackagerConsole;
|
@ -1,6 +0,0 @@
|
||||
import 'source-map-support/register';
|
||||
import 'babel-polyfill';
|
||||
|
||||
import buildApp from './build/buildMain';
|
||||
|
||||
export default buildApp;
|
@ -1,4 +0,0 @@
|
||||
export { default as inferIcon } from './inferIcon';
|
||||
export { default as inferOs } from './inferOs';
|
||||
export { default as inferTitle } from './inferTitle';
|
||||
export { default as inferUserAgent } from './inferUserAgent';
|
@ -1,130 +0,0 @@
|
||||
import pageIcon from 'page-icon';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import tmp from 'tmp';
|
||||
import gitCloud from 'gitcloud';
|
||||
import helpers from '../helpers/helpers';
|
||||
|
||||
const { downloadFile, allowedIconFormats } = helpers;
|
||||
tmp.setGracefulCleanup();
|
||||
|
||||
const GITCLOUD_SPACE_DELIMITER = '-';
|
||||
|
||||
function getMaxMatchScore(iconWithScores) {
|
||||
return iconWithScores.reduce((maxScore, currentIcon) => {
|
||||
const currentScore = currentIcon.score;
|
||||
if (currentScore > maxScore) {
|
||||
return currentScore;
|
||||
}
|
||||
return maxScore;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* also maps ext to icon object
|
||||
*/
|
||||
function getMatchingIcons(iconsWithScores, maxScore) {
|
||||
return iconsWithScores
|
||||
.filter((item) => item.score === maxScore)
|
||||
.map((item) => Object.assign({}, item, { ext: path.extname(item.url) }));
|
||||
}
|
||||
|
||||
function mapIconWithMatchScore(fileIndex, targetUrl) {
|
||||
const normalisedTargetUrl = targetUrl.toLowerCase();
|
||||
return fileIndex.map((item) => {
|
||||
const itemWords = item.name.split(GITCLOUD_SPACE_DELIMITER);
|
||||
const score = itemWords.reduce((currentScore, word) => {
|
||||
if (normalisedTargetUrl.includes(word)) {
|
||||
return currentScore + 1;
|
||||
}
|
||||
return currentScore;
|
||||
}, 0);
|
||||
|
||||
return Object.assign({}, item, { score });
|
||||
});
|
||||
}
|
||||
|
||||
function inferIconFromStore(targetUrl, platform) {
|
||||
const allowedFormats = new Set(allowedIconFormats(platform));
|
||||
|
||||
return gitCloud('https://jiahaog.github.io/nativefier-icons/').then(
|
||||
(fileIndex) => {
|
||||
const iconWithScores = mapIconWithMatchScore(fileIndex, targetUrl);
|
||||
const maxScore = getMaxMatchScore(iconWithScores);
|
||||
|
||||
if (maxScore === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconsMatchingScore = getMatchingIcons(iconWithScores, maxScore);
|
||||
const iconsMatchingExt = iconsMatchingScore.filter((icon) =>
|
||||
allowedFormats.has(icon.ext),
|
||||
);
|
||||
const matchingIcon = iconsMatchingExt[0];
|
||||
const iconUrl = matchingIcon && matchingIcon.url;
|
||||
|
||||
if (!iconUrl) {
|
||||
return null;
|
||||
}
|
||||
return downloadFile(iconUrl);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function writeFilePromise(outPath, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(outPath, data, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(outPath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function inferFromPage(targetUrl, platform, outDir) {
|
||||
let preferredExt = '.png';
|
||||
if (platform === 'win32') {
|
||||
preferredExt = '.ico';
|
||||
}
|
||||
|
||||
// todo might want to pass list of preferences instead
|
||||
return pageIcon(targetUrl, { ext: preferredExt }).then((icon) => {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const outfilePath = path.join(outDir, `/icon${icon.ext}`);
|
||||
return writeFilePromise(outfilePath, icon.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} targetUrl
|
||||
* @param {string} platform
|
||||
* @param {string} outDir
|
||||
*/
|
||||
function inferIconFromUrlToPath(targetUrl, platform, outDir) {
|
||||
return inferIconFromStore(targetUrl, platform).then((icon) => {
|
||||
if (!icon) {
|
||||
return inferFromPage(targetUrl, platform, outDir);
|
||||
}
|
||||
|
||||
const outfilePath = path.join(outDir, `/icon${icon.ext}`);
|
||||
return writeFilePromise(outfilePath, icon.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} targetUrl
|
||||
* @param {string} platform
|
||||
*/
|
||||
function inferIcon(targetUrl, platform) {
|
||||
const tmpObj = tmp.dirSync({ unsafeCleanup: true });
|
||||
const tmpPath = tmpObj.name;
|
||||
return inferIconFromUrlToPath(targetUrl, platform, tmpPath);
|
||||
}
|
||||
|
||||
export default inferIcon;
|
111
src/infer/inferIcon.ts
Normal file
111
src/infer/inferIcon.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import * as path from 'path';
|
||||
import { writeFile } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as gitCloud from 'gitcloud';
|
||||
import * as pageIcon from 'page-icon';
|
||||
|
||||
import {
|
||||
downloadFile,
|
||||
getAllowedIconFormats,
|
||||
getTempDir,
|
||||
} from '../helpers/helpers';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
const writeFileAsync = promisify(writeFile);
|
||||
|
||||
const GITCLOUD_SPACE_DELIMITER = '-';
|
||||
const GITCLOUD_URL = 'https://jiahaog.github.io/nativefier-icons/';
|
||||
|
||||
function getMaxMatchScore(iconWithScores: any[]): number {
|
||||
const score = iconWithScores.reduce((maxScore, currentIcon) => {
|
||||
const currentScore = currentIcon.score;
|
||||
if (currentScore > maxScore) {
|
||||
return currentScore;
|
||||
}
|
||||
return maxScore;
|
||||
}, 0);
|
||||
log.debug('Max icon match score:', score);
|
||||
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 mapIconWithMatchScore(cloudIcons: any[], targetUrl: string): any {
|
||||
const normalisedTargetUrl = targetUrl.toLowerCase();
|
||||
return cloudIcons.map((item) => {
|
||||
const itemWords = item.name.split(GITCLOUD_SPACE_DELIMITER);
|
||||
const score = itemWords.reduce((currentScore, word) => {
|
||||
if (normalisedTargetUrl.includes(word)) {
|
||||
return currentScore + 1;
|
||||
}
|
||||
return currentScore;
|
||||
}, 0);
|
||||
|
||||
return { ...item, score };
|
||||
});
|
||||
}
|
||||
|
||||
async function inferIconFromStore(
|
||||
targetUrl: string,
|
||||
platform: string,
|
||||
): Promise<any> {
|
||||
log.debug(`Inferring icon from store for ${targetUrl} on ${platform}`);
|
||||
const allowedFormats = new Set(getAllowedIconFormats(platform));
|
||||
|
||||
const cloudIcons = await gitCloud(GITCLOUD_URL);
|
||||
log.debug(`Got ${cloudIcons.length} icons from gitcloud`);
|
||||
const iconWithScores = mapIconWithMatchScore(cloudIcons, targetUrl);
|
||||
const maxScore = getMaxMatchScore(iconWithScores);
|
||||
|
||||
if (maxScore === 0) {
|
||||
log.debug('No relevant icon in store.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconsMatchingScore = getMatchingIcons(iconWithScores, maxScore);
|
||||
const iconsMatchingExt = iconsMatchingScore.filter((icon) =>
|
||||
allowedFormats.has(icon.ext),
|
||||
);
|
||||
const matchingIcon = iconsMatchingExt[0];
|
||||
const iconUrl = matchingIcon && matchingIcon.url;
|
||||
|
||||
if (!iconUrl) {
|
||||
log.debug('Could not infer icon from store');
|
||||
return null;
|
||||
}
|
||||
return downloadFile(iconUrl);
|
||||
}
|
||||
|
||||
export async function inferIcon(
|
||||
targetUrl: string,
|
||||
platform: string,
|
||||
): Promise<string> {
|
||||
log.debug(`Inferring icon for ${targetUrl} on ${platform}`);
|
||||
const tmpDirPath = getTempDir('iconinfer');
|
||||
|
||||
let icon: { ext: string; data: Buffer } = 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;
|
||||
}
|
||||
log.debug(`Got an icon from the page.`);
|
||||
|
||||
const iconPath = path.join(tmpDirPath, `/icon${icon.ext}`);
|
||||
log.debug(
|
||||
`Writing ${(icon.data.length / 1024).toFixed(1)} kb icon to ${iconPath}`,
|
||||
);
|
||||
await writeFileAsync(iconPath, icon.data);
|
||||
return iconPath;
|
||||
}
|
@ -1,28 +1,27 @@
|
||||
import os from 'os';
|
||||
import * as os from 'os';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
function inferPlatform() {
|
||||
export function inferPlatform(): string {
|
||||
const platform = os.platform();
|
||||
if (
|
||||
platform === 'darwin' ||
|
||||
// @ts-ignore
|
||||
platform === 'mas' ||
|
||||
platform === 'win32' ||
|
||||
platform === 'linux'
|
||||
) {
|
||||
log.debug('Inferred platform', platform);
|
||||
return platform;
|
||||
}
|
||||
|
||||
throw new Error(`Untested platform ${platform} detected`);
|
||||
}
|
||||
|
||||
function inferArch() {
|
||||
export function inferArch(): string {
|
||||
const arch = os.arch();
|
||||
if (arch !== 'ia32' && arch !== 'x64' && arch !== 'arm') {
|
||||
throw new Error(`Incompatible architecture ${arch} detected`);
|
||||
}
|
||||
log.debug('Inferred arch', arch);
|
||||
return arch;
|
||||
}
|
||||
|
||||
export default {
|
||||
inferPlatform,
|
||||
inferArch,
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import cheerio from 'cheerio';
|
||||
|
||||
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';
|
||||
|
||||
function inferTitle(url) {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url,
|
||||
headers: {
|
||||
// fake a user agent because pages like http://messenger.com will throw 404 error
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
};
|
||||
|
||||
return axios(options).then(({ data }) => {
|
||||
const $ = cheerio.load(data);
|
||||
return $('title')
|
||||
.first()
|
||||
.text();
|
||||
});
|
||||
}
|
||||
|
||||
export default inferTitle;
|
@ -1,21 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import inferTitle from './inferTitle';
|
||||
|
||||
jest.mock('axios', () =>
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: `
|
||||
<HTML>
|
||||
<head>
|
||||
<title>TEST_TITLE</title>
|
||||
</head>
|
||||
</HTML>`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
test('it returns the correct title', async () => {
|
||||
const result = await inferTitle('someurl');
|
||||
expect(axios).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBe('TEST_TITLE');
|
||||
});
|
19
src/infer/inferTitle.test.ts
Normal file
19
src/infer/inferTitle.test.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { inferTitle } from './inferTitle';
|
||||
|
||||
test('it returns the correct title', async () => {
|
||||
const axiosGetMock = jest.spyOn(axios, 'get');
|
||||
axiosGetMock.mockResolvedValue({
|
||||
data: `
|
||||
<HTML>
|
||||
<head>
|
||||
<title>TEST_TITLE</title>
|
||||
</head>
|
||||
</HTML>`,
|
||||
});
|
||||
const result = await inferTitle('someurl');
|
||||
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBe('TEST_TITLE');
|
||||
});
|
23
src/infer/inferTitle.ts
Normal file
23
src/infer/inferTitle.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
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, {
|
||||
headers: {
|
||||
// Fake user agent for pages like http://messenger.com
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
});
|
||||
log.debug(`Fetched ${(data.length / 1024).toFixed(1)} kb page at`, url);
|
||||
const $ = cheerio.load(data);
|
||||
const inferredTitle = $('title')
|
||||
.first()
|
||||
.text();
|
||||
|
||||
log.debug('Inferred title:', inferredTitle);
|
||||
return inferredTitle;
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import _ from 'lodash';
|
||||
import log from 'loglevel';
|
||||
|
||||
const ELECTRON_VERSIONS_URL = 'https://atom.io/download/atom-shell/index.json';
|
||||
const DEFAULT_CHROME_VERSION = '61.0.3163.100';
|
||||
|
||||
function getChromeVersionForElectronVersion(
|
||||
electronVersion,
|
||||
url = ELECTRON_VERSIONS_URL,
|
||||
) {
|
||||
return axios.get(url, { timeout: 5000 }).then((response) => {
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Bad request: Status code ${response.status}`);
|
||||
}
|
||||
|
||||
const { data } = response;
|
||||
const electronVersionToChromeVersion = _.zipObject(
|
||||
data.map((d) => d.version),
|
||||
data.map((d) => d.chrome),
|
||||
);
|
||||
|
||||
if (!(electronVersion in electronVersionToChromeVersion)) {
|
||||
throw new Error(
|
||||
`Electron version '${electronVersion}' not found in retrieved version list!`,
|
||||
);
|
||||
}
|
||||
|
||||
return electronVersionToChromeVersion[electronVersion];
|
||||
});
|
||||
}
|
||||
|
||||
export function getUserAgentString(chromeVersion, platform) {
|
||||
let userAgent;
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
case 'mas':
|
||||
userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
break;
|
||||
case 'win32':
|
||||
userAgent = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
break;
|
||||
case 'linux':
|
||||
userAgent = `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
'Error invalid platform specified to getUserAgentString()',
|
||||
);
|
||||
}
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
function inferUserAgent(
|
||||
electronVersion,
|
||||
platform,
|
||||
url = ELECTRON_VERSIONS_URL,
|
||||
) {
|
||||
return getChromeVersionForElectronVersion(electronVersion, url)
|
||||
.then((chromeVersion) => getUserAgentString(chromeVersion, platform))
|
||||
.catch(() => {
|
||||
log.warn(
|
||||
`Unable to infer chrome version for user agent, using ${DEFAULT_CHROME_VERSION}`,
|
||||
);
|
||||
return getUserAgentString(DEFAULT_CHROME_VERSION, platform);
|
||||
});
|
||||
}
|
||||
|
||||
export default inferUserAgent;
|
@ -1,37 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import inferUserAgent from './inferUserAgent';
|
||||
|
||||
const TEST_RESULT = {
|
||||
darwin:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36',
|
||||
mas:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36',
|
||||
win32:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36',
|
||||
linux:
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36',
|
||||
};
|
||||
|
||||
function testPlatform(platform) {
|
||||
return expect(inferUserAgent('0.37.1', platform)).resolves.toBe(
|
||||
TEST_RESULT[platform],
|
||||
);
|
||||
}
|
||||
|
||||
describe('Infer User Agent', () => {
|
||||
test('Can infer userAgent for all platforms', async () => {
|
||||
const testPromises = _.keys(TEST_RESULT).map((platform) =>
|
||||
testPlatform(platform),
|
||||
);
|
||||
await Promise.all(testPromises);
|
||||
});
|
||||
|
||||
test('Connection error will still get a user agent', async () => {
|
||||
jest.setTimeout(6000);
|
||||
|
||||
const TIMEOUT_URL = 'http://www.google.com:81/';
|
||||
await expect(inferUserAgent('1.6.7', 'darwin', TIMEOUT_URL)).resolves.toBe(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
|
||||
);
|
||||
});
|
||||
});
|
29
src/infer/inferUserAgent.test.ts
Normal file
29
src/infer/inferUserAgent.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { inferUserAgent } from './inferUserAgent';
|
||||
import { DEFAULT_ELECTRON_VERSION, DEFAULT_CHROME_VERSION } from '../constants';
|
||||
|
||||
const EXPECTED_USERAGENTS = {
|
||||
darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${DEFAULT_CHROME_VERSION} Safari/537.36`,
|
||||
mas: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${DEFAULT_CHROME_VERSION} Safari/537.36`,
|
||||
win32: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${DEFAULT_CHROME_VERSION} Safari/537.36`,
|
||||
linux: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${DEFAULT_CHROME_VERSION} Safari/537.36`,
|
||||
};
|
||||
|
||||
describe('Infer User Agent', () => {
|
||||
test('Can infer userAgent for all platforms', async () => {
|
||||
jest.setTimeout(10000);
|
||||
for (const [arch, archUa] of Object.entries(EXPECTED_USERAGENTS)) {
|
||||
const ua = await inferUserAgent(DEFAULT_ELECTRON_VERSION, arch);
|
||||
expect(ua).toBe(archUa);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO make fast by mocking timeout, and un-skip
|
||||
test.skip('Connection error will still get a user agent', async () => {
|
||||
jest.setTimeout(6000);
|
||||
|
||||
const TIMEOUT_URL = 'http://www.google.com:81/';
|
||||
await expect(inferUserAgent('1.6.7', 'darwin', TIMEOUT_URL)).resolves.toBe(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
|
||||
);
|
||||
});
|
||||
});
|
82
src/infer/inferUserAgent.ts
Normal file
82
src/infer/inferUserAgent.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as _ from 'lodash';
|
||||
import axios from 'axios';
|
||||
import * as log from 'loglevel';
|
||||
import { DEFAULT_CHROME_VERSION } from '../constants';
|
||||
|
||||
const ELECTRON_VERSIONS_URL = 'https://atom.io/download/atom-shell/index.json';
|
||||
|
||||
async function getChromeVersionForElectronVersion(
|
||||
electronVersion: string,
|
||||
url = ELECTRON_VERSIONS_URL,
|
||||
): Promise<string> {
|
||||
log.debug('Grabbing electron<->chrome versions file from', url);
|
||||
const response = await axios.get(url, { timeout: 5000 });
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Bad request: Status code ${response.status}`);
|
||||
}
|
||||
const { data } = response;
|
||||
const electronVersionToChromeVersion: _.Dictionary<string> = _.zipObject(
|
||||
data.map((d) => d.version),
|
||||
data.map((d) => d.chrome),
|
||||
);
|
||||
if (!(electronVersion in electronVersionToChromeVersion)) {
|
||||
throw new Error(
|
||||
`Electron version '${electronVersion}' not found in retrieved version list!`,
|
||||
);
|
||||
}
|
||||
const chromeVersion = electronVersionToChromeVersion[electronVersion];
|
||||
log.debug(
|
||||
`Associated electron v${electronVersion} to chrome v${chromeVersion}`,
|
||||
);
|
||||
return chromeVersion;
|
||||
}
|
||||
|
||||
export function getUserAgentString(
|
||||
chromeVersion: string,
|
||||
platform: string,
|
||||
): string {
|
||||
let userAgent: string;
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
case 'mas':
|
||||
userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
break;
|
||||
case 'win32':
|
||||
userAgent = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
break;
|
||||
case 'linux':
|
||||
userAgent = `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
'Error invalid platform specified to getUserAgentString()',
|
||||
);
|
||||
}
|
||||
log.debug(
|
||||
`Given chrome ${chromeVersion} on ${platform},`,
|
||||
`using user agent: ${userAgent}`,
|
||||
);
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
export async function inferUserAgent(
|
||||
electronVersion: string,
|
||||
platform: string,
|
||||
url = ELECTRON_VERSIONS_URL,
|
||||
): Promise<string> {
|
||||
log.debug(
|
||||
`Inferring user agent for electron ${electronVersion} / ${platform}`,
|
||||
);
|
||||
try {
|
||||
const chromeVersion = await getChromeVersionForElectronVersion(
|
||||
electronVersion,
|
||||
url,
|
||||
);
|
||||
return getUserAgentString(chromeVersion, platform);
|
||||
} catch (e) {
|
||||
log.warn(
|
||||
`Unable to infer chrome version for user agent, using ${DEFAULT_CHROME_VERSION}`,
|
||||
);
|
||||
return getUserAgentString(DEFAULT_CHROME_VERSION, platform);
|
||||
}
|
||||
}
|
57
src/integration-test.ts
Normal file
57
src/integration-test.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getTempDir } from './helpers/helpers';
|
||||
import { buildNativefierApp } from './main';
|
||||
|
||||
function checkApp(appRoot: string, inputOptions: any): void {
|
||||
let relativeAppFolder: string;
|
||||
|
||||
switch (inputOptions.platform) {
|
||||
case 'darwin':
|
||||
relativeAppFolder = path.join('Google.app', 'Contents/Resources/app');
|
||||
break;
|
||||
case 'linux':
|
||||
relativeAppFolder = 'resources/app';
|
||||
break;
|
||||
case 'win32':
|
||||
relativeAppFolder = 'resources/app';
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown app platform');
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Test name inferring
|
||||
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.statSync(iconPath).size).toBeGreaterThan(1000);
|
||||
}
|
||||
|
||||
describe('Nativefier', () => {
|
||||
jest.setTimeout(300000);
|
||||
|
||||
test('builds a Nativefier app for several platforms', async () => {
|
||||
for (const platform of ['darwin', 'linux']) {
|
||||
const tempDirectory = getTempDir('integtest');
|
||||
const options = {
|
||||
targetUrl: 'https://google.com/',
|
||||
out: tempDirectory,
|
||||
overwrite: true,
|
||||
platform,
|
||||
};
|
||||
const appPath = await buildNativefierApp(options);
|
||||
checkApp(appPath, options);
|
||||
}
|
||||
});
|
||||
});
|
7
src/jestSetupFiles.ts
Normal file
7
src/jestSetupFiles.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as log from 'loglevel';
|
||||
|
||||
if (process.env.LOGLEVEL) {
|
||||
log.setLevel(process.env.LOGLEVEL as log.LogLevelDesc);
|
||||
} else {
|
||||
log.disableAll();
|
||||
}
|
20
src/main.ts
Normal file
20
src/main.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import 'source-map-support/register';
|
||||
|
||||
import { buildNativefierApp } from './build/buildNativefierApp';
|
||||
|
||||
export { buildNativefierApp };
|
||||
|
||||
/**
|
||||
* Only for compatibility with Nativefier <= 7.7.1 !
|
||||
* Use the better, modern async `buildNativefierApp` instead if you can!
|
||||
*/
|
||||
function buildNativefierAppOldCallbackStyle(
|
||||
options: any,
|
||||
callback: (err: any, result?: any) => void,
|
||||
): void {
|
||||
buildNativefierApp(options)
|
||||
.then((result) => callback(null, result))
|
||||
.catch((err) => callback(err));
|
||||
}
|
||||
|
||||
export default buildNativefierAppOldCallbackStyle;
|
@ -1,22 +0,0 @@
|
||||
import fields from './fields';
|
||||
|
||||
function resultArrayToObject(fieldResults) {
|
||||
return fieldResults.reduce(
|
||||
(accumulator, value) => Object.assign({}, accumulator, value),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
function inferredOptions(oldOptions, fieldResults) {
|
||||
const newOptions = resultArrayToObject(fieldResults);
|
||||
return Object.assign({}, oldOptions, newOptions);
|
||||
}
|
||||
|
||||
// Takes the options object and infers new values
|
||||
// which may need async work
|
||||
export default function(options) {
|
||||
const tasks = fields(options);
|
||||
return Promise.all(tasks).then((fieldResults) =>
|
||||
inferredOptions(options, fieldResults),
|
||||
);
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import asyncConfig from './asyncConfig';
|
||||
import fields from './fields';
|
||||
|
||||
jest.mock('./fields');
|
||||
|
||||
fields.mockImplementation(() => [
|
||||
Promise.resolve({
|
||||
someField: 'newValue',
|
||||
}),
|
||||
]);
|
||||
|
||||
test('it should merge the result of the promise', async () => {
|
||||
const param = { another: 'field', someField: 'oldValue' };
|
||||
const expected = { another: 'field', someField: 'newValue' };
|
||||
|
||||
const result = await asyncConfig(param);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
12
src/options/asyncConfig.ts
Normal file
12
src/options/asyncConfig.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { processOptions } from './fields/fields';
|
||||
import { AppOptions } from './model';
|
||||
|
||||
/**
|
||||
* Takes the options object and infers new values needing async work
|
||||
*/
|
||||
export async function asyncConfig(options: AppOptions): Promise<any> {
|
||||
log.debug('\nPerforming async options post-processing.');
|
||||
await processOptions(options);
|
||||
}
|
36
src/options/fields/fields.test.ts
Normal file
36
src/options/fields/fields.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { processOptions } from './fields';
|
||||
|
||||
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);
|
||||
|
||||
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 inferred if not passed', 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).toMatch(/Linux.*Chrome/);
|
||||
});
|
29
src/options/fields/fields.ts
Normal file
29
src/options/fields/fields.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { icon } from './icon';
|
||||
import { userAgent } from './userAgent';
|
||||
import { AppOptions } from '../model';
|
||||
import { name } from './name';
|
||||
|
||||
const OPTION_POSTPROCESSORS = [
|
||||
{ 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> {
|
||||
const processedOptions = await Promise.all(
|
||||
OPTION_POSTPROCESSORS.map(async ({ namespace, option, processor }) => {
|
||||
const result = await processor(options);
|
||||
return {
|
||||
namespace,
|
||||
option,
|
||||
result,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
for (const { namespace, option, result } of processedOptions) {
|
||||
if (result !== null) {
|
||||
options[namespace][option] = result;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import log from 'loglevel';
|
||||
import { inferIcon } from '../../infer';
|
||||
|
||||
export default function({ icon, targetUrl, platform }) {
|
||||
// Icon is the path to the icon
|
||||
if (icon) {
|
||||
return Promise.resolve(icon);
|
||||
}
|
||||
|
||||
return inferIcon(targetUrl, platform).catch((error) => {
|
||||
log.warn('Cannot automatically retrieve the app icon:', error);
|
||||
return null;
|
||||
});
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import log from 'loglevel';
|
||||
import icon from './icon';
|
||||
import { inferIcon } from '../../infer';
|
||||
|
||||
jest.mock('./../../infer/inferIcon');
|
||||
jest.mock('loglevel');
|
||||
|
||||
const mockedResult = 'icon path';
|
||||
|
||||
describe('when the icon parameter is passed', () => {
|
||||
test('it should return the icon parameter', async () => {
|
||||
expect(inferIcon).toHaveBeenCalledTimes(0);
|
||||
|
||||
const params = { icon: './icon.png' };
|
||||
await expect(icon(params)).resolves.toBe(params.icon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the icon parameter is not passed', () => {
|
||||
test('it should call inferIcon', async () => {
|
||||
inferIcon.mockImplementationOnce(() => Promise.resolve(mockedResult));
|
||||
const params = { targetUrl: 'some url', platform: 'mac' };
|
||||
|
||||
const result = await icon(params);
|
||||
|
||||
expect(result).toBe(mockedResult);
|
||||
expect(inferIcon).toHaveBeenCalledWith(params.targetUrl, params.platform);
|
||||
});
|
||||
|
||||
describe('when inferIcon resolves with an error', () => {
|
||||
test('it should handle the error', async () => {
|
||||
inferIcon.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error('some error')),
|
||||
);
|
||||
const params = { targetUrl: 'some url', platform: 'mac' };
|
||||
|
||||
const result = await icon(params);
|
||||
expect(result).toBe(null);
|
||||
expect(inferIcon).toHaveBeenCalledWith(params.targetUrl, params.platform);
|
||||
expect(log.warn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
60
src/options/fields/icon.test.ts
Normal file
60
src/options/fields/icon.test.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { icon } from './icon';
|
||||
import { inferIcon } from '../../infer/inferIcon';
|
||||
|
||||
jest.mock('./../../infer/inferIcon');
|
||||
jest.mock('loglevel');
|
||||
|
||||
const mockedResult = 'icon path';
|
||||
const ICON_PARAMS_PROVIDED = {
|
||||
packager: {
|
||||
icon: './icon.png',
|
||||
targetUrl: 'https://google.com',
|
||||
platform: 'mac',
|
||||
},
|
||||
};
|
||||
const ICON_PARAMS_NEEDS_INFER = {
|
||||
packager: {
|
||||
targetUrl: 'https://google.com',
|
||||
platform: 'mac',
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the icon parameter is not passed', () => {
|
||||
test('it should call inferIcon', async () => {
|
||||
(inferIcon as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve(mockedResult),
|
||||
);
|
||||
const result = await icon(ICON_PARAMS_NEEDS_INFER);
|
||||
|
||||
expect(result).toBe(mockedResult);
|
||||
expect(inferIcon).toHaveBeenCalledWith(
|
||||
ICON_PARAMS_NEEDS_INFER.packager.targetUrl,
|
||||
ICON_PARAMS_NEEDS_INFER.packager.platform,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when inferIcon resolves with an error', () => {
|
||||
test('it should handle the error', async () => {
|
||||
(inferIcon as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.reject(new Error('some error')),
|
||||
);
|
||||
const result = await icon(ICON_PARAMS_NEEDS_INFER);
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(inferIcon).toHaveBeenCalledWith(
|
||||
ICON_PARAMS_NEEDS_INFER.packager.targetUrl,
|
||||
ICON_PARAMS_NEEDS_INFER.packager.platform,
|
||||
);
|
||||
expect(log.warn).toHaveBeenCalledTimes(1); // eslint-disable-line @typescript-eslint/unbound-method
|
||||
});
|
||||
});
|
||||
});
|
28
src/options/fields/icon.ts
Normal file
28
src/options/fields/icon.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { inferIcon } from '../../infer/inferIcon';
|
||||
|
||||
type IconParams = {
|
||||
packager: {
|
||||
icon?: string;
|
||||
targetUrl: string;
|
||||
platform?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function icon(options: IconParams): Promise<string> {
|
||||
if (options.packager.icon) {
|
||||
log.debug('Got icon from options. Using it, no inferring needed');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await inferIcon(
|
||||
options.packager.targetUrl,
|
||||
options.packager.platform,
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn('Cannot automatically retrieve the app icon:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import icon from './icon';
|
||||
import userAgent from './userAgent';
|
||||
import name from './name';
|
||||
|
||||
const fields = [
|
||||
{
|
||||
field: 'userAgent',
|
||||
task: userAgent,
|
||||
},
|
||||
{
|
||||
field: 'icon',
|
||||
task: icon,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
task: name,
|
||||
},
|
||||
];
|
||||
|
||||
// Modifies the result of each promise from a scalar
|
||||
// value to a object containing its fieldname
|
||||
function wrap(fieldName, promise, args) {
|
||||
return promise(args).then((result) => ({
|
||||
[fieldName]: result,
|
||||
}));
|
||||
}
|
||||
|
||||
// Returns a list of promises which will all resolve
|
||||
// with the following result: {[fieldName]: fieldvalue}
|
||||
export default function(options) {
|
||||
return fields.map(({ field, task }) => wrap(field, task, options));
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import fields from './index';
|
||||
import icon from './icon';
|
||||
import userAgent from './userAgent';
|
||||
import name from './name';
|
||||
|
||||
jest.mock('./icon');
|
||||
jest.mock('./name');
|
||||
jest.mock('./userAgent');
|
||||
|
||||
const modules = [icon, userAgent, name];
|
||||
modules.forEach((module) => {
|
||||
module.mockImplementation(() => Promise.resolve());
|
||||
});
|
||||
|
||||
test('it should return a list of promises', () => {
|
||||
const result = fields({});
|
||||
expect(result).toHaveLength(3);
|
||||
result.forEach((value) => {
|
||||
expect(value).toBeInstanceOf(Promise);
|
||||
});
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
import log from 'loglevel';
|
||||
import { sanitizeFilename } from '../../utils';
|
||||
import { inferTitle } from '../../infer';
|
||||
import { DEFAULT_APP_NAME } from '../../constants';
|
||||
|
||||
function tryToInferName({ name, targetUrl }) {
|
||||
// .length also checks if its the commanderJS function or a string
|
||||
if (name && name.length > 0) {
|
||||
return Promise.resolve(name);
|
||||
}
|
||||
|
||||
return inferTitle(targetUrl)
|
||||
.then((pageTitle) => pageTitle || DEFAULT_APP_NAME)
|
||||
.catch((error) => {
|
||||
log.warn(
|
||||
`Unable to automatically determine app name, falling back to '${DEFAULT_APP_NAME}'. Reason: ${error}`,
|
||||
);
|
||||
return DEFAULT_APP_NAME;
|
||||
});
|
||||
}
|
||||
|
||||
export default function({ platform, name, targetUrl }) {
|
||||
return tryToInferName({ name, targetUrl }).then((result) =>
|
||||
sanitizeFilename(platform, result),
|
||||
);
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
import log from 'loglevel';
|
||||
import name from './name';
|
||||
import { DEFAULT_APP_NAME } from '../../constants';
|
||||
import { inferTitle } from '../../infer';
|
||||
import { sanitizeFilename } from '../../utils';
|
||||
|
||||
jest.mock('./../../infer/inferTitle');
|
||||
jest.mock('./../../utils/sanitizeFilename');
|
||||
jest.mock('loglevel');
|
||||
|
||||
sanitizeFilename.mockImplementation((_, filename) => filename);
|
||||
|
||||
const mockedResult = 'mock name';
|
||||
|
||||
describe('well formed name parameters', () => {
|
||||
const params = { name: 'appname', platform: 'something' };
|
||||
test('it should not call inferTitle', async () => {
|
||||
const result = await name(params);
|
||||
|
||||
expect(inferTitle).toHaveBeenCalledTimes(0);
|
||||
expect(result).toBe(params.name);
|
||||
});
|
||||
|
||||
test('it should call sanitize filename', async () => {
|
||||
const result = await name(params);
|
||||
expect(sanitizeFilename).toHaveBeenCalledWith(params.platform, result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bad name parameters', () => {
|
||||
beforeEach(() => {
|
||||
inferTitle.mockImplementationOnce(() => Promise.resolve(mockedResult));
|
||||
});
|
||||
|
||||
const params = { targetUrl: 'some url' };
|
||||
describe('when the name is undefined', () => {
|
||||
test('it should call inferTitle', async () => {
|
||||
await name(params);
|
||||
expect(inferTitle).toHaveBeenCalledWith(params.targetUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the name is an empty string', () => {
|
||||
test('it should call inferTitle', async () => {
|
||||
const testParams = {
|
||||
...params,
|
||||
name: '',
|
||||
};
|
||||
|
||||
await name(testParams);
|
||||
expect(inferTitle).toHaveBeenCalledWith(params.targetUrl);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should call sanitize filename', () =>
|
||||
name(params).then((result) => {
|
||||
expect(sanitizeFilename).toHaveBeenCalledWith(params.platform, result);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('handling inferTitle results', () => {
|
||||
const params = { targetUrl: 'some url', name: '', platform: 'something' };
|
||||
test('it should return the result from inferTitle', async () => {
|
||||
inferTitle.mockImplementationOnce(() => Promise.resolve(mockedResult));
|
||||
|
||||
const result = await name(params);
|
||||
expect(result).toBe(mockedResult);
|
||||
expect(inferTitle).toHaveBeenCalledWith(params.targetUrl);
|
||||
});
|
||||
|
||||
describe('when the returned pageTitle is falsey', () => {
|
||||
test('it should return the default app name', async () => {
|
||||
inferTitle.mockImplementationOnce(() => Promise.resolve(null));
|
||||
|
||||
const result = await name(params);
|
||||
expect(result).toBe(DEFAULT_APP_NAME);
|
||||
expect(inferTitle).toHaveBeenCalledWith(params.targetUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inferTitle resolves with an error', () => {
|
||||
test('it should return the default app name', async () => {
|
||||
inferTitle.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error('some error')),
|
||||
);
|
||||
|
||||
const result = await name(params);
|
||||
expect(result).toBe(DEFAULT_APP_NAME);
|
||||
expect(inferTitle).toHaveBeenCalledWith(params.targetUrl);
|
||||
expect(log.warn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
108
src/options/fields/name.test.ts
Normal file
108
src/options/fields/name.test.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { name } from './name';
|
||||
import { DEFAULT_APP_NAME } from '../../constants';
|
||||
import { inferTitle } from '../../infer/inferTitle';
|
||||
import { sanitizeFilename } from '../../utils/sanitizeFilename';
|
||||
|
||||
jest.mock('./../../infer/inferTitle');
|
||||
jest.mock('./../../utils/sanitizeFilename');
|
||||
jest.mock('loglevel');
|
||||
|
||||
const inferTitleMockedResult = 'mock name';
|
||||
const NAME_PARAMS_PROVIDED = {
|
||||
packager: {
|
||||
name: 'appname',
|
||||
targetUrl: 'https://google.com',
|
||||
platform: 'linux',
|
||||
},
|
||||
};
|
||||
const NAME_PARAMS_NEEDS_INFER = {
|
||||
packager: {
|
||||
targetUrl: 'https://google.com',
|
||||
platform: 'mac',
|
||||
},
|
||||
};
|
||||
beforeAll(() => {
|
||||
(sanitizeFilename as jest.Mock).mockImplementation((_, filename) => filename);
|
||||
});
|
||||
|
||||
describe('well formed name parameters', () => {
|
||||
test('it should not call inferTitle', async () => {
|
||||
const result = await name(NAME_PARAMS_PROVIDED);
|
||||
|
||||
expect(inferTitle).toHaveBeenCalledTimes(0);
|
||||
expect(result).toBe(NAME_PARAMS_PROVIDED.packager.name);
|
||||
});
|
||||
|
||||
test('it should call sanitize filename', async () => {
|
||||
const result = await name(NAME_PARAMS_PROVIDED);
|
||||
|
||||
expect(sanitizeFilename).toHaveBeenCalledWith(
|
||||
NAME_PARAMS_PROVIDED.packager.platform,
|
||||
result,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bad name parameters', () => {
|
||||
beforeEach(() => {
|
||||
(inferTitle as jest.Mock).mockResolvedValue(inferTitleMockedResult);
|
||||
});
|
||||
|
||||
const params = { packager: { targetUrl: 'some url', platform: 'whatever' } };
|
||||
test('it should call inferTitle when the name is undefined', async () => {
|
||||
await name(params);
|
||||
expect(inferTitle).toHaveBeenCalledWith(params.packager.targetUrl);
|
||||
});
|
||||
|
||||
test('it should call inferTitle when the name is an empty string', async () => {
|
||||
const testParams = {
|
||||
...params,
|
||||
name: '',
|
||||
};
|
||||
|
||||
await name(testParams);
|
||||
expect(inferTitle).toHaveBeenCalledWith(params.packager.targetUrl);
|
||||
});
|
||||
|
||||
test('it should call sanitize filename', async () => {
|
||||
const result = await name(params);
|
||||
expect(sanitizeFilename).toHaveBeenCalledWith(
|
||||
params.packager.platform,
|
||||
result,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling inferTitle results', () => {
|
||||
test('it should return the result from inferTitle', async () => {
|
||||
const result = await name(NAME_PARAMS_NEEDS_INFER);
|
||||
|
||||
expect(result).toEqual(inferTitleMockedResult);
|
||||
expect(inferTitle).toHaveBeenCalledWith(
|
||||
NAME_PARAMS_NEEDS_INFER.packager.targetUrl,
|
||||
);
|
||||
});
|
||||
|
||||
test('it should return the default app name when the returned pageTitle is falsey', async () => {
|
||||
(inferTitle as jest.Mock).mockResolvedValue(null);
|
||||
const result = await name(NAME_PARAMS_NEEDS_INFER);
|
||||
|
||||
expect(result).toEqual(DEFAULT_APP_NAME);
|
||||
expect(inferTitle).toHaveBeenCalledWith(
|
||||
NAME_PARAMS_NEEDS_INFER.packager.targetUrl,
|
||||
);
|
||||
});
|
||||
|
||||
test('it should return the default app name when inferTitle rejects', async () => {
|
||||
(inferTitle as jest.Mock).mockRejectedValue('some error');
|
||||
const result = await name(NAME_PARAMS_NEEDS_INFER);
|
||||
|
||||
expect(result).toEqual(DEFAULT_APP_NAME);
|
||||
expect(inferTitle).toHaveBeenCalledWith(
|
||||
NAME_PARAMS_NEEDS_INFER.packager.targetUrl,
|
||||
);
|
||||
expect(log.warn).toHaveBeenCalledTimes(1); // eslint-disable-line @typescript-eslint/unbound-method
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user