2
2
mirror of https://github.com/Llewellynvdm/nativefier.git synced 2024-11-11 15:51:06 +00:00

Compare commits

...

385 Commits

Author SHA1 Message Date
Jia Hao
9078a3071a
Nativefier is unmaintained 2023-09-29 21:45:16 +08:00
Adam Weeden
45d7981761 Update changelog for v52.0.0 2023-08-25 14:11:56 -04:00
Adam Weeden
2e0fe9ea33
Update to Electron 25.7 (#1566) 2023-08-25 14:07:42 -04:00
Adam Weeden
64157c3c5d
Update to Electron 25 (#1559)
This is intended to get Electron updated to 25. There are no known bugs
in this release.

As well this includes a fix for an existing bug I noticed where child
windows in Windows received a menu bar when they should not have.
2023-08-25 09:10:05 -04:00
Adam Weeden
be418d4349 Update changelog for v51.0.1 2023-08-04 18:36:48 -04:00
Adam Weeden
c39932731d
npm i in the Dockerfile to esnure we have what we need to build + test (#1557) 2023-08-03 17:44:36 -04:00
Adam Weeden
dea954bea8 Update changelog for v51.0.0 2023-08-03 14:43:33 -04:00
Adam Weeden
051622d58e
Update Electron to 21 + Node to 16 (#1550) 2023-08-03 13:35:19 -04:00
Matthew Ruzzi
fae8edd183
Update link to Development Guide (#1544) 2023-07-20 12:33:49 -04:00
Adam Weeden
443d6cacdf Update changelog for v50.1.1 2023-03-27 13:28:23 -04:00
Adam Weeden
739c90b756 Fix shrinkwrap versions back to lockfileVersion 1 (node 12) 2023-03-27 13:27:15 -04:00
Tobias
0d33985c46
Fix typo "electon" -> "electron" (#1492) 2023-03-24 17:07:58 -04:00
Adam Weeden
be3b0cf5de Update changelog for v50.1.0 2023-03-24 17:05:47 -04:00
Adam Weeden
cef8e42251 Fix directory and add clarification for new smoke test 4 2023-03-24 16:48:16 -04:00
Adam Weeden
dacfcd2cb8 Add some type hinting post-package update for Axios 2023-03-24 16:47:54 -04:00
Adam Weeden
6c867925bb Update outdated shrinkwrap files 2023-03-24 16:19:05 -04:00
Adam Weeden
69625f1a01
Fix broken tests (#1523) 2023-03-24 14:16:49 -04:00
Johan von Forstner
79009e87cd
Add getDisplayMedia and PipeWire support (#1477)
I'm picking up @RickStanley's abandoned PR #1321 again to add
screensharing support (fixes #927), with the following additional
changes:

- In newer Electron versions, `desktopCapturer.getSources` must be
called from the main process, so I solved this with an IPC call.
- Importing from `./helpers/helpers` in 'preload.ts' does not work, as
was mentioned by @DimICE in
https://github.com/nativefier/nativefier/pull/1321#issuecomment-1001518035.
I'm not very familiar with TypeScript or Electron, so not sure why that
is and how it could be solved - for now I just copied the referenced
`isWayland` function to `preload.ts`.
- Add a screensharing test to the manual test script, as requested by
@ronjouch in
https://github.com/nativefier/nativefier/pull/1321#issuecomment-1006725818

As far as I understood from the discussion in #1321, the last point was
basically the only thing that was missing to get this merged, correct?

---------

Co-authored-by: Rick Stanley <rick-stanley@outlook.com>
Co-authored-by: Rick Stanley <rick.stanley@lambda3.com.br>
Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2023-03-23 11:50:19 -04:00
Ronan Jouchet
8d3396acc3 ESLint: scrap unused var 2022-11-07 18:37:03 -05:00
Ronan Jouchet
ec0c1461ee Update changelog for v50.0.1 2022-11-07 18:29:29 -05:00
Ronan Jouchet
379e807bc2 Bump default Electron from 19.0.17 to 19.1.4 2022-11-07 18:18:23 -05:00
Ronan Jouchet
2335dbce7d Bump axios from 0.x to 1.x. No breaking changes for us, https://github.com/axios/axios/blob/v1.x/CHANGELOG.md#100---2022-10-04 2022-11-07 18:12:12 -05:00
Ronan Jouchet
f42e94646a Relock app dependencies 2022-11-07 18:08:34 -05:00
Ronan Jouchet
14f8535050 Relock CLI dependencies 2022-11-07 18:07:51 -05:00
Adam777
f046b61a6d
Maximize window visual glitch on Windows fix (fix #1447) (PR #1448)
Fix for [Maximize window visual glitch on Windows · Issue #1447 · nativefier/nativefier](https://github.com/nativefier/nativefier/issues/1447)

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2022-11-07 17:55:11 -05:00
Ronan Jouchet
1fd046798d CI: test on 12 and **19**, now that 19 is out 2022-11-07 17:34:20 -05:00
Ronan Jouchet
ba7244f77a External URL protocols: add zoommtg as no-confirmation
Also, document the current state of things.
Follow-up of https://github.com/nativefier/nativefier/pull/1463 .
2022-11-07 17:29:28 -05:00
Alvaro
f22750b41c
CATALOG.md: Microsoft Teams with css inject (PR #1469)
* Microsoft Teams with custom css injected

I wasted some time trying to setup Microsoft Teams using nativefier, and some issues pointed me in the right direction. I decided to not only stop there, but to inject some css to hide some unnecessary elements from a desktop app, like an app download button and a waffle button that sits on the top left corner.

* Update CATALOG.md

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2022-10-17 16:40:01 -04:00
Alvaro
229bc71935
CATALOG.md: WhatsApp custom CSS for native-looking app on macOS (#1468)
Added injected css to make WhatsApp more native-like in macOS
2022-10-04 22:30:18 -04:00
Ronan Jouchet
b310315604 Update changelog for v50.0.0 2022-09-17 22:48:01 -04:00
Ronan Jouchet
cc957e70d1 Bump default Electron to 19.0.17 (from .14), with security fixes
- https://github.com/electron/electron/releases/tag/v19.0.15
- https://github.com/electron/electron/releases/tag/v19.0.16
- https://github.com/electron/electron/releases/tag/v19.0.17
2022-09-17 22:35:29 -04:00
Ronan Jouchet
93205accd5 relock app 2022-09-17 22:31:05 -04:00
Ronan Jouchet
f53827a8e0 relock cli 2022-09-17 22:30:33 -04:00
Ronan Jouchet
840fe4a199 Add validation to opening URLs in external desktop handler (fix #1459) 2022-09-17 22:22:19 -04:00
Ronan Jouchet
eecc33c1aa Fix double-navigation to pages
setupNativefierWindow is already called in createMainWindow !
No need to call it again here in browser-window-created!
Double-navs weren't visible with internal links but they were with external links
2022-09-17 22:02:10 -04:00
Ronan Jouchet
be2180b53f Update changelog for v49.0.1 2022-08-28 21:04:15 -04:00
Ronan Jouchet
5f1e2cbf55 Revert node types to 12.x, from 14.x accidentally set which is wrong since we still support 12.x 2022-08-28 21:01:19 -04:00
Ronan Jouchet
1933f226cc Bump default Electron to 19.0.14 (from .10)
- https://github.com/electron/electron/releases/tag/v19.0.11
- https://github.com/electron/electron/releases/tag/v19.0.12
- https://github.com/electron/electron/releases/tag/v19.0.13
- https://github.com/electron/electron/releases/tag/v19.0.14
2022-08-28 20:56:28 -04:00
Ronan Jouchet
f337401e34 relock app 2022-08-28 20:42:16 -04:00
Ronan Jouchet
c1a47ae6a8 relock cli 2022-08-28 20:41:15 -04:00
Adam Weeden
c8fc0b6923
macOS: Move handling of "universal" apps to electron-packager instead of our own thing (#1443)
Supported since [electron-packager 15.5.0](https://github.com/electron/electron-packager/releases/tag/v15.5.0).

This should fix #1405  as well
2022-08-01 22:44:23 -04:00
Ronan Jouchet
357f4a9693 Update changelog for v49.0.0 2022-07-30 13:48:46 -04:00
Sirisak Lueangsaksri
57636b9022
macOS: Fix "main window cannot be activated" (fix #1415, PR #1417) 2022-07-30 13:31:18 -04:00
Ronan Jouchet
b362fbec3b Relock deps to get gitcloud 0.2.4 with "fetch" log fix
https://github.com/nativefier/gitcloud-client/pull/4
2022-07-30 13:27:39 -04:00
Ronan Jouchet
f59564fd6d Bump default Electron to 19.0.10
https://github.com/electron/electron/releases/tag/v19.0.10
2022-07-30 13:25:52 -04:00
Adam Weeden
347c06d250
Remove arch ia32 as it's no longer supported in Electron 19 (fix #1439, PR #1441) 2022-07-30 13:19:45 -04:00
Adam Weeden
82ff609a91
Fix playwright tests on Linux (#1440) 2022-07-30 13:15:30 -04:00
Ronan Jouchet
0de2463f2b Docker: actually let's use *lts*-alpine, for more futureproof-ness 2022-07-24 12:26:22 -04:00
Ronan Jouchet
c725243152 Docker: upgrade base node-alpine img from 12 to 18
Node 12 makes PlayWright complain,
https://github.com/nativefier/nativefier/runs/7489079540?check_suite_focus=true
2022-07-24 12:25:09 -04:00
Ronan Jouchet
73bc316c99 HACKING / major-upgrading Electron: link to blog 2022-07-24 12:13:06 -04:00
Ronan Jouchet
01ef78e37c Update changelog for v48.0.0 2022-07-24 12:12:32 -04:00
Ronan Jouchet
1aa7760aa2
Major-bump default Electron from 18 to 19 (#1438)
https://github.com/electron/electron/releases/tag/v19.0.0
https://github.com/electron/electron/releases/tag/v19.0.1
https://github.com/electron/electron/releases/tag/v19.0.2
https://github.com/electron/electron/releases/tag/v19.0.3
https://github.com/electron/electron/releases/tag/v19.0.4
https://github.com/electron/electron/releases/tag/v19.0.5
https://github.com/electron/electron/releases/tag/v19.0.6
https://github.com/electron/electron/releases/tag/v19.0.7
https://github.com/electron/electron/releases/tag/v19.0.8
https://github.com/electron/electron/releases/tag/v19.0.9
2022-07-24 11:56:07 -04:00
Ronan Jouchet
f6e585b3cc relock app 2022-07-24 11:25:33 -04:00
Ronan Jouchet
1f67a9f9a7
Bump jest to 28 (#1437)
https://jestjs.io/blog/2022/04/25/jest-28
https://jestjs.io/docs/upgrading-to-jest28
https://github.com/facebook/jest/blob/main/CHANGELOG.md#2800

Co-authored-by: Adam Weeden <adamweeden@gmail.com>
2022-07-24 11:24:59 -04:00
Ronan Jouchet
a4ef7481de Make URL override smarter (thx Adam)
to not crash local PlayWright tests that here might get
Electron command-line options.

See https://github.com/nativefier/nativefier/pull/1437#issuecomment-1193315259
2022-07-24 10:34:31 -04:00
Ronan Jouchet
e12da46064 Playwright tests: document DEBUG mode 2022-07-23 20:05:48 -04:00
Ronan Jouchet
c3e010f881 re-introduce yargs fix but without upgrading jest to 28 breaking tests, to work on it separately 2022-07-23 19:27:26 -04:00
Ronan Jouchet
9db7ad050a Revert "nit" and "bump jest and fix yargs typing"
This reverts commit b737c40931.
This reverts commit 8eb05ded9b.
2022-07-23 19:20:24 -04:00
Ronan Jouchet
b737c40931 nit 2022-07-23 19:10:44 -04:00
Ronan Jouchet
8eb05ded9b bump jest and fix yargs typing 2022-07-23 19:07:54 -04:00
Ronan Jouchet
207bb1790f bump jest **types**, so far it still builds 2022-07-23 18:43:09 -04:00
Ronan Jouchet
6b3db5de11 relock cli 2022-07-23 18:39:04 -04:00
Ronan Jouchet
d499b5f1c3 bug.yml & feature.yml & question.yml: more 2022-07-23 18:31:24 -04:00
Ronan Jouchet
14c61af12c bug.yml & feature.yml & question.yml: more 2022-07-23 18:30:03 -04:00
Ronan Jouchet
79ff80ca53 bug.yml & feature.yml & question.yml: more 2022-07-23 18:28:22 -04:00
Ronan Jouchet
eebdf90bc7 bug.yml & feature.yml: more 2022-07-23 18:23:58 -04:00
Ronan Jouchet
7fdc4b8515 bug.yml & feature.yml: more 2022-07-23 18:21:33 -04:00
Ronan Jouchet
a042812dca bug.yml & feature.yml: more 2022-07-23 18:19:39 -04:00
Ronan Jouchet
7ab50a0e2d bug.yml: more 2022-07-23 18:05:25 -04:00
Ronan Jouchet
8e6363a634 bug.yml: more 2022-07-23 18:04:37 -04:00
Ronan Jouchet
dae20c62e1 bug.yml: more 2022-07-23 17:59:45 -04:00
Ronan Jouchet
6c48f5b013 bug.yml: more 2022-07-23 17:58:41 -04:00
Ronan Jouchet
9c099ce6bc bug.yml: more 2022-07-23 17:57:05 -04:00
Ronan Jouchet
4967e47aee bug.yml: more 2022-07-23 17:55:28 -04:00
Ronan Jouchet
4f38119682 bug.yml: first edits 2022-07-23 17:54:23 -04:00
Matthew Ruzzi
9ed379024d
Switch issues from GitHub "issue templates" to new/beta "issue forms" (fix #1258) (#1425) 2022-07-23 17:43:48 -04:00
Octavio Ietsugu
8907b9dc97
CATALOG.md: add a new recipe for using interactive buttons on Notion (#1430)
I've used Nativefier along with Notion in the last couple days and I was able to use interactive buttons on my Notion exported page, something that was not possible originally. As such, please consider the following recipe.

Co-authored-by: Dev pop-os <dev+pop-os@mevio.com.br>
2022-07-23 17:39:00 -04:00
Ronan Jouchet
c1d40ec181 Update changelog for v47.2.1 2022-06-27 16:01:22 -04:00
Ronan Jouchet
7221b39a2f Bump default Electron to 18.3.5
https://github.com/electron/electron/releases/tag/v18.3.2
https://github.com/electron/electron/releases/tag/v18.3.3
https://github.com/electron/electron/releases/tag/v18.3.4
https://github.com/electron/electron/releases/tag/v18.3.5
2022-06-27 15:56:57 -04:00
Ronan Jouchet
eeac8885cc Relock dependencies 2022-06-27 15:49:33 -04:00
Ronan Jouchet
bac08394c5 macOS: fix incorrect "Back" keyboard shortcut (fix #1426) 2022-06-27 09:34:13 -04:00
Ronan Jouchet
5146dc05e5 README: shorter 2022-05-30 12:26:05 -04:00
Ronan Jouchet
1d56cd77bc README: leftover 2022-05-30 12:23:57 -04:00
Ronan Jouchet
4d0b913133 Update changelog for v47.2.0 2022-05-30 10:22:03 -04:00
Ronan Jouchet
ea0381f226 README: more readable 2022-05-30 10:18:34 -04:00
Ronan Jouchet
3f3108464c CATALOG: cleanup 2022-05-30 10:16:15 -04:00
Ronan Jouchet
0d99e98c2a CATALOG: move in troubleshooting previously in README 2022-05-30 10:15:03 -04:00
Ronan Jouchet
c82f1a892e Bump default Electron to 18.3.1
https://github.com/electron/electron/releases/tag/v18.2.1
https://github.com/electron/electron/releases/tag/v18.2.2
https://github.com/electron/electron/releases/tag/v18.2.3
https://github.com/electron/electron/releases/tag/v18.2.4
https://github.com/electron/electron/releases/tag/v18.3.0
https://github.com/electron/electron/releases/tag/v18.3.1
2022-05-30 09:58:42 -04:00
Ronan Jouchet
b123ebdd77 README: shorter 2022-05-30 09:52:47 -04:00
Ronan Jouchet
4dd92ef060 README: layout 2022-05-30 09:51:43 -04:00
Ronan Jouchet
cbd6ee2c68 README: stop mentioning unmaintained icon repo 2022-05-30 09:51:27 -04:00
Ronan Jouchet
c9c81f5583 README: attempt to be more concise, using gh "details" feature 2022-05-30 09:50:28 -04:00
Ronan Jouchet
c0210265ab Relock dependencies 2022-05-30 09:42:40 -04:00
Sirisak Lueangsaksri
0df5b8f617
Handle 'open-url' event: support "deep-linking" e.g. for mailto links (PR #1418, fix #1412) 2022-05-30 09:40:13 -04:00
Ronan Jouchet
59a4bb87f9 API.md: fix link to Electron Frameless Window (fix #1382) 2022-05-19 23:04:00 -04:00
Ronan Jouchet
4c6d0b185b Update changelog for v47.1.3 2022-05-02 00:32:00 -04:00
Ronan Jouchet
ed33836707 Revert accidental leftover package.json changes 2022-05-02 00:31:49 -04:00
Ronan Jouchet
052d5c75a5 Update changelog for v47.1.2 2022-05-02 00:29:21 -04:00
Ronan Jouchet
334cbe28a5 Publish: fix Docker build failing because of Playwright, and make a convenience npm task for it 2022-05-02 00:27:58 -04:00
Ronan Jouchet
8bbc7cacbd CI: restore needing successful playwright build to publish 2022-05-02 00:19:18 -04:00
Ronan Jouchet
0afff67c11 Update changelog for v47.1.1 2022-05-02 00:13:31 -04:00
Ronan Jouchet
27979cfd42 Fix publish.yml running on ubuntu and thus not able to run playwright tests 2022-05-02 00:13:09 -04:00
Ronan Jouchet
26bd19d6b0 Update changelog for v47.1.0 2022-05-02 00:09:07 -04:00
Ronan Jouchet
eb81f0c3b2 CI: re-order tasks, for friendlier display in GH's popup limited to 6 lines
This should let us see without scrolling: lint, playwright, and latest-node*
2022-05-01 23:58:37 -04:00
Ronan Jouchet
78e330aa87 Bump default Electron to 18.2.0, from 18.0.3
https://github.com/electron/electron/releases/tag/v18.0.4
https://github.com/electron/electron/releases/tag/v18.1.0
https://github.com/electron/electron/releases/tag/v18.2.0
2022-05-01 23:55:21 -04:00
Ronan Jouchet
0963402264 Revert "Major-bump jest to 28": breaks tsc in weird ways, will retry later.
This reverts commit bd109099ce.
2022-05-01 23:51:04 -04:00
Ronan Jouchet
bd109099ce Major-bump jest to 28
No breaking changes for us, I think:
https://jestjs.io/blog/2022/04/25/jest-28
https://jestjs.io/docs/upgrading-to-jest28
2022-05-01 23:30:26 -04:00
Ronan Jouchet
0ff6e87308 Major bump axios to 0.27.x
No breaking changes for us: https://github.com/axios/axios/blob/master/CHANGELOG.md
2022-05-01 23:24:57 -04:00
Ronan Jouchet
a95814f5d4 Relock dependencies 2022-05-01 23:22:57 -04:00
Ronan Jouchet
c3f0e4777f CATALOG.md: move new "general recipes" section where it belongs, and cleanup 2022-05-01 23:19:58 -04:00
ecarril6
0ab6bb6aed
CATALOG.md: add "Window Size and Position" section (PR #1349) 2022-05-01 23:17:26 -04:00
Ronan Jouchet
1e1c720aa9 CI: you cannot win 2022-05-01 23:10:11 -04:00
Ronan Jouchet
eacbaf737f CI: yes yes 2022-05-01 23:06:56 -04:00
Ronan Jouchet
458c7ec178 CI: okay GHA, no variables 2022-05-01 23:06:25 -04:00
Ronan Jouchet
6af4e774e2 CI: fumbling around 2022-05-01 23:02:05 -04:00
Ronan Jouchet
430a129c39 CI: (attempt to) separate/parallelize Playwright tests, for speed 2022-05-01 22:59:39 -04:00
Ronan Jouchet
887347adbb Playwright tests: only run on windows, because mac keeps failing or being too slow 2022-04-26 19:55:30 -04:00
Ronan Jouchet
339fbfb933 Bump minimum macOS version from 10.9 to 10.10 (see #1404) 2022-04-26 19:40:16 -04:00
Adam Weeden
d85aab718d
Extend timeout for playwright electron launch + playwright 1.21.1 (#1402) 2022-04-25 10:11:11 -04:00
Adam Weeden
ce04b3337c
Add some debugging to the playwright script + add a timeout for the playwright ci (#1400) 2022-04-22 10:51:00 -04:00
Simon Smith
513b9dc93d
Windows: correctly set notifications name - not electron.app.YOURAPPNAME (PR #1394)
This sets the name correctly for Windows notifications.
Currently, notifications name shows as `electron.app.YOURAPPNAME`
2022-04-21 08:54:36 -04:00
Ronan Jouchet
60035a8e74 Bump max tested version of Node for CI/Publish from 17 to 18
Released yesterday: https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V18.md#18.0.0
2022-04-20 22:06:46 -04:00
Adam Weeden
e664bc6af8
Add playwright integration testing to the app (PR #1397)
This PR allows us to code playwright integration tests that can potentially replace some of our manual tests and allow us automated testing of the app itself.

Current technical limitations:
* No app level keyboard simulation support (e.g, zoom in, zoom out, etc.)
* No code coverage support, so even though we are testing the app, the code coverage does not reflect this fact
2022-04-20 22:03:49 -04:00
Adam Weeden
c42c63a8b0
Fix universal app path logic to resolve to absolute (fix #1398) 2022-04-19 09:56:16 -04:00
Adam Weeden
3a8f66a7b6
macOS: universal architecture app support (fix #1384, PR #1386)
As noted in `API.md`:

- When specifying `universal` you must be building for the `darwin`, `mas`, `mac`, or `osx` platforms. This will generate a universal (M1 and x86) app.
2022-04-18 19:11:31 -04:00
Adam Weeden
10fb19b377
macOS: fix Open In New Tab (fix #1260, PR #1385) 2022-04-18 19:06:44 -04:00
Pedro Algarvio
74bc2d4188
Auto-internal URLs: add VMWare Workspace ONE + SecurID (PR #1391, fix #1390)
Update internal URLs to include VMware's Workspace ONE and SecurID.
2022-04-18 18:46:31 -04:00
Adam Weeden
f6852d5208
Update browser versions + add fix for Firefox user agent (#1388) 2022-04-18 18:42:25 -04:00
Adam Weeden
f6a1e30085
Change Paste and Match Style shortcut to match Apple's HIG advice (PR #1387, fix #404) 2022-04-18 18:41:27 -04:00
Tristan Koch
6a949ca481
--counter: accept colon character; useful for time-tracking apps with hour:min in title (PR #1378)
Modify regexp to track hour counters such as `(1:23)`.

This is supported by macOS' dock and displays in the red badge:
https://user-images.githubusercontent.com/73974/162786478-609a90e1-5efb-44ba-9aa5-7a3038f0689b.png
2022-04-11 13:18:06 -04:00
Ronan Jouchet
567fede701 Update changelog for v47.0.0 2022-04-10 13:30:12 -04:00
Ronan Jouchet
77df5618f4 Bump default Electron to 18.0.3 (from 16.2.2)
As usual, we did our best to adapt to Electron breaking changes in 17/18,
but patches welcome to fix regressions. Release notes with breaking changes:

- https://www.electronjs.org/blog/electron-17-0
- https://www.electronjs.org/blog/electron-18-0

Detailed release notes:

- https://github.com/electron/electron/releases/tag/v17.0.0
- https://github.com/electron/electron/releases/tag/v18.0.0
- https://github.com/electron/electron/releases/tag/v18.0.1
- https://github.com/electron/electron/releases/tag/v18.0.2
- https://github.com/electron/electron/releases/tag/v18.0.3
2022-04-10 13:18:58 -04:00
Ronan Jouchet
c215d7a7b2 Update changelog for v46.2.1 2022-04-10 11:32:04 -04:00
Ronan Jouchet
94c4131e39 Bump default Electron to 16.2.2 2022-04-10 11:22:30 -04:00
Ronan Jouchet
26e9569b25 Bump default Electron to 16.2.1 (from 16.1.0), with security fixes:
- https://github.com/electron/electron/releases/tag/v16.1.1
- https://github.com/electron/electron/releases/tag/v16.2.0
- https://github.com/electron/electron/releases/tag/v16.2.1
2022-04-10 11:20:40 -04:00
Ronan Jouchet
6f61d73d11 Upgrade dependencies lockfiles
with the usual `npm run relock` .
No major/breaking chages this time.
2022-04-10 11:18:41 -04:00
Ronan Jouchet
2477b59dda Update changelog for v46.2.0 2022-03-20 23:44:36 -04:00
Ronan Jouchet
e06f6d929d Bump default Electron to 16.1.0 (from 16.0.9), with security fixes:
- https://github.com/electron/electron/releases/tag/v16.0.10
- https://github.com/electron/electron/releases/tag/v16.1.0
2022-03-20 23:36:07 -04:00
Ronan Jouchet
036122ad55 Upgrade dependencies lockfiles
with the usual `npm run relock` .
No major/breaking chages this time.
2022-03-20 23:32:19 -04:00
Tedward747
83dce91c47
Strip LRM and RLM in Linux names (fix #1361, PR #1365)
On Linux (SUSE at least), if `--name` isn't provided and is inferred,
the filename is prepended with the control character
LRM (https://en.wikipedia.org/wiki/Left-to-right_mark) and I'd assume
RLM for anyone who's using that setting.

This PR strips those control characters out of the file name.
2022-03-20 23:27:57 -04:00
Tedward747
2f97a7156b
Remove extra whitespace in UserAgent (fix #1357, PR #1367)
Fixes https://github.com/nativefier/nativefier/issues/1357 , by
not adding a new space when removing the app name from the User-Agent.

Co-authored-by: noxafy <hci@gmx.de>
2022-03-20 23:23:10 -04:00
Tedward747
8dbe7943d4
API.md: Update link for conceal flag (#1364)
Old link went 404, new one has an overview of what it is and has a link to the asar github for anyone wanting technical knowledge
2022-03-02 15:35:48 -05:00
Ronan Jouchet
388eebb58b Update changelog for v46.1.1 2022-02-14 23:06:37 -05:00
Ronan Jouchet
ad74271b98 Bump default Electron to 16.0.9, from 16.0.8 2022-02-14 23:03:47 -05:00
Ronan Jouchet
5bcf165fe4 Upgrade dependencies
Including axios 0.25 -> 0.25, nothing breaking for us:
https://github.com/axios/axios/releases/tag/v0.26.0
2022-02-14 23:01:36 -05:00
Abhishek Mehandiratta
64ed2a856b
Feature: Add "copy as plain text" in edit menu (PR #1351, fix #1144)
This closes #1144 by adding a "Copy as plain text" function in Edit menu.

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2022-02-10 08:39:56 -05:00
Ronan Jouchet
35fb0fa7ff Update changelog for v46.1.0 2022-02-06 18:08:11 -05:00
Ronan Jouchet
5ce6c24a3f Fix lint not running in CI 2022-02-06 17:55:20 -05:00
Ronan Jouchet
5de8a307ab Bump default Electron to 16.0.8, from 16.0.6
- https://github.com/electron/electron/releases/tag/v16.0.7
- https://github.com/electron/electron/releases/tag/v16.0.8
2022-02-06 17:53:45 -05:00
Ronan Jouchet
6d46082220 Bump axios from 0.24.0 to 0.25
See https://github.com/axios/axios/blob/master/CHANGELOG.md#0250-january-18-2022
No obvious breaking changes, I think.
2022-02-06 17:51:20 -05:00
Ronan Jouchet
347df98c77 Make eslint happy 2022-02-06 17:49:30 -05:00
Ronan Jouchet
6b6ef1d12d Relock dependencies 2022-02-06 17:47:06 -05:00
Henry Bridge
9945a5dffe
Add flag --strict-internal-urls to disable domain and subpath matching (PR #1340)
I created this so that Google Meet links don't open in my Google Calendar app for me, but it looks like others have a similar issue (e.g. issue #1304).
2022-02-06 17:40:51 -05:00
Abhishek Mehandiratta
372c1d0b13
Fixes ignored --file-download-options (PR #1350, fix #1275) 2022-02-06 00:02:49 -05:00
Ronan Jouchet
e7483549af NATIVEFIER_APPS_DIR: document "new in release", rephrase doc to be the most useful to a first-time user 2022-01-31 17:07:57 -05:00
Matthew Ruzzi
c6debd72e0
Allow setting default app destination with env. var. NATIVEFIER_APPS_DIR (PR #1339, #1336) 2022-01-31 16:59:23 -05:00
Ronan Jouchet
f8bd696e32 README: mention Snap & AUR repos
See https://github.com/nativefier/nativefier/pull/1348
2022-01-31 16:33:53 -05:00
Ronan Jouchet
088be2d258
HACKING.md: triage guidelines (PR #1338)
As discussed in https://github.com/nativefier/nativefier/issues/1258#issuecomment-1008522842 ,
here’s an attempt to formalize triage,

1. To agree on our triage practices.
2. To ease up recruiting more triagers.
3. To have a reference to point at when a user doesn't understand how we're triaging.
2022-01-31 16:23:47 -05:00
Zachary Talis
31fc580f1c
API.md: Fix broken "insecurity options" link (PR #1345) 2022-01-28 23:01:56 -05:00
Tyler Nickerson
aeb6ba1a8c
Add "quiet" flag to suppress all log output (PR #1342)
In working on my own repo [hop](https://github.com/Nickersoft/hop), which uses nativefier under-the-hood, I found it very troublesome to suppress log output when using the programmatic API. This PR just adds a quick "quiet" option which will set the log level to "silent" and suppress all electron and nativefier output (except for errors, of course).

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2022-01-23 16:01:29 -05:00
Ronan Jouchet
cd7bd26d3c HACKING.md: link to Hickey 2022-01-10 00:42:36 -05:00
Ronan Jouchet
2274053792 HACKING: markdown format 2022-01-10 00:03:47 -05:00
Ronan Jouchet
8d178b2507 CATALOG.md: Document GCal needs lying about useragent for working notifications (fix #1292) 2022-01-09 08:42:32 -05:00
Ronan Jouchet
25c4da6b4d README: more more more more more nits 2022-01-06 14:33:49 -05:00
Ronan Jouchet
1b30ba5cbb README: more more more more nits 2022-01-06 14:32:43 -05:00
Ronan Jouchet
f88244746e README: more more more nits 2022-01-06 14:21:16 -05:00
Ronan Jouchet
8a8a5dd2cc README: more more nits 2022-01-06 14:20:41 -05:00
Ronan Jouchet
37e2f98801 README: more nits 2022-01-06 14:20:12 -05:00
Ronan Jouchet
2445395ad3 README: nits 2022-01-06 14:18:22 -05:00
Ronan Jouchet
8d05fc9b8b Docs: link to Docker Hub repo, lighten README and move some stuff to CATALOG 2022-01-06 14:09:15 -05:00
Ronan Jouchet
e4ab2dba5b Update changelog for v46.0.4 2022-01-06 10:50:42 -05:00
Ronan Jouchet
ae4a0f05b8 CI: (Attempt to) push tag, not unreadable SHA
Might have to retry this one as I'm not sure it's this one or GITHUB_REF,
https://docs.github.com/en/actions/learn-github-actions/environment-variables
2022-01-06 10:50:00 -05:00
Ronan Jouchet
776f2a846d Update changelog for v46.0.3 2022-01-06 10:33:22 -05:00
Ronan Jouchet
97cce9196d CI: (Attempt to) push image to our org, not my personal account 2022-01-06 10:32:48 -05:00
Ronan Jouchet
863377d814 Update changelog for v46.0.2 2022-01-06 10:16:45 -05:00
Pranav Shikarpur
7b4d172248
CI: Fix Docker Hub image build & push (#1100)
We tried using the auto Docker Hub thingie, but it's flaky.
Reverting to manually pushing as part of release CI.
2022-01-06 10:14:31 -05:00
Ronan Jouchet
0a561a62c0 Update changelog for v46.0.1 2022-01-06 09:58:15 -05:00
Ronan Jouchet
e3a823c66b Bump default Electron from 16.0.5 to 16.0.6
https://github.com/electron/electron/releases/tag/v16.0.6
2022-01-06 09:43:10 -05:00
Ronan Jouchet
5e3c23267a Fix --widevine broken since 46.0.0
See https://github.com/nativefier/nativefier/pull/1288#issuecomment-1006307669 ,
"the version prefix changed from -wvvmp to +wvcus",

and message from CastLabs:
The v16 series of Electron for Content Security, labeled wvcus, moves to using the Component Updater Service to handle installation of the Widevine CDM, and has incompatible API updates compared to the previous wvvmp releases.
2022-01-06 09:34:20 -05:00
Ronan Jouchet
5276a839bb Update changelog for v46.0.0 2022-01-02 06:31:08 -05:00
Adam Weeden
d483597320
Upgrade Electron from 13 to 16 (PR #1288)
https://www.electronjs.org/docs/latest/breaking-changes

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2022-01-02 06:17:06 -05:00
Ronan Jouchet
6f4ae587c4 build/ci workflow: pass the node version number with the format expected by GHA/setup-node
and avoid warning "Not found in manifest.  Falling back to download directly from Node"
2021-12-25 16:53:06 -05:00
Ronan Jouchet
16ec3b80fe empty commit to see if cache is indeed used and build is faster (npm ci used to take between 30s and 1m10s) 2021-12-25 16:46:59 -05:00
Ronan Jouchet
797922afb6 build/ci workflow: try to use setup-node-v2 cache to speed up build 2021-12-25 16:42:48 -05:00
Ronan Jouchet
24e8849564 Update changelog for v45.0.8 2021-12-06 23:48:19 -05:00
Ronan Jouchet
e33aa4ebab Fix 45.0.7 broken because of missing "chalk" dep (fix #1324) 2021-12-06 23:45:09 -05:00
Ronan Jouchet
e9e523d957 Update changelog for v45.0.7 2021-12-06 16:27:13 -05:00
Ronan Jouchet
dab770f719 app pkgjson too 2021-12-06 16:12:40 -05:00
Ronan Jouchet
f720f66eb8 Bump default Electron to 13.6.3 with fixes & security fixes
https://github.com/electron/electron/releases/tag/v13.6.3
2021-12-06 16:12:03 -05:00
Ronan Jouchet
58e504de32 document contextIsolation stuff 2021-12-06 16:05:41 -05:00
Ronan Jouchet
ec77741494 re-relock and fix script failing to do app if cli failed 2021-12-06 16:00:17 -05:00
Ronan Jouchet
df4ca5d079 relock dependencies 2021-12-06 15:56:02 -05:00
Adam Weeden
5f02f14626
Use userAgentFallback for userAgent injection (PR #1316)
Per the suggestion of @fireflinchdev , this seems to alleviate our issues with things like nuking service workers for WhatsApp. The core reason being those service workers were getting the original userAgent, and not the override (if specified). This PR should fix that.

So it should resolve #1312 #719 and hopefully a few future issues as this seems to come up frequently.
2021-11-29 16:53:12 -05:00
Adam Weeden
b9c5e2b464
Fix upgrade not working (#1286)
* Attempt to get upgrade working right; in progress

* Got it fixed in Mac

* Fix some linting errors

* Finish fixing upgrade + tests

* Integration testing for global shortcuts

* Regenerate shrinkwrap

* Get rid of deprecated rmdirSync

* Remove instead of rm for 12.x support

* Make dereferencing platform dependent

* Fix folder copy funkiness

* Whoops

* Whoops 2: Extra Whoops

* Update Electron to 13.5.1; Fix auth manual tests

* Rework relock

* Add a request for help.

* Update @types/node to 14
2021-11-29 12:01:20 -05:00
Ronan Jouchet
adcf7c4c0c Update changelog for v45.0.6 2021-11-22 16:24:24 -05:00
Ronan Jouchet
4405b5d28b Fix icon conversion scripts broken on recent macOS (fix #1277) 2021-11-22 16:18:02 -05:00
Ronan Jouchet
702ff1e4b6 Bump default Electron to 13.6.2
https://github.com/electron/electron/releases/tag/v13.6.2
2021-11-22 16:05:20 -05:00
Ronan Jouchet
601968dfef Relock dependencies 2021-11-22 16:03:07 -05:00
Alexander Weps
ca7d25f432
Fix notifications broken since Nativefier 43 / Electron 12 defaulting to contextIsolation:true (PR #1308)
Copy-pastaing details from [Electron 12 breaking changes](https://www.electronjs.org/docs/latest/breaking-changes#planned-breaking-api-changes-120):

> ### Default Changed: `contextIsolation` defaults to `true`[​](https://www.electronjs.org/docs/latest/breaking-changes#default-changed-contextisolation-defaults-to-true "Direct link to heading")
> 
> In Electron 12, `contextIsolation` will be enabled by default. To restore the previous behavior, `contextIsolation: false` must be specified in WebPreferences.
> 
> We [recommend having contextIsolation enabled](https://www.electronjs.org/docs/latest/tutorial/security#3-enable-context-isolation-for-remote-content) for the security of your application.
> 
> Another implication is that `require()` cannot be used in the renderer process unless `nodeIntegration` is `true` and `contextIsolation` is `false`.
> 
> For more details see: [https://github.com/electron/electron/issues/23506](https://github.com/electron/electron/issues/23506)

I find the security drop acceptable, as reverting the new Electron 12 isolation brings us to the previous level of security, and I don't have the time/will to keep the isolation and migrate to the newer better safer thing that Electron >= 12 wants.

Co-authored-by: Radomír Polách <rp@t4d.cz>
2021-11-22 16:00:13 -05:00
Ronan Jouchet
431f531065 CI: bump top Node.js version to 17, which is now stable 2021-11-10 18:41:38 -05:00
Ronan Jouchet
e63a275ec9 Update changelog for v45.0.5 2021-11-01 15:59:22 -04:00
Ronan Jouchet
916055d05a generate-changelog: don't break if npm out returns 1, which it normally does on outdated deps, which is all the time 2021-11-01 15:57:33 -04:00
Ronan Jouchet
d68078f686 Bump axios to 0.24.0
https://github.com/axios/axios/blob/master/CHANGELOG.md
2021-11-01 15:54:03 -04:00
Ronan Jouchet
7cab31f974 Bump to eslint 8
https://eslint.org/docs/8.0.0/user-guide/migrating-to-8.0.0
2021-11-01 15:47:46 -04:00
Ronan Jouchet
94e6c6cf54 Bump default Electron to 13.6.1, with security fixes
- 13.5.2: https://github.com/electron/electron/releases/tag/v13.5.2
- 13.6.0: https://github.com/electron/electron/releases/tag/v13.6.0
- 13.6.1: https://github.com/electron/electron/releases/tag/v13.6.1
2021-11-01 15:37:33 -04:00
Ronan Jouchet
05e2439c56 HACKING.md: Document one more place to look when major-upgrading Electron 2021-10-04 09:32:52 -04:00
Adam Weeden
0d99ce4916
Update Electron to 13.5.1; Fix auth manual tests (#1287) 2021-10-02 22:17:06 -04:00
Ronan Jouchet
fadcb73de7 Update changelog for v45.0.4 2021-09-24 23:03:48 -04:00
Ronan Jouchet
0570a20c99 Work around "npm shrinkwrap" failing to include some packages in lockfile 2021-09-24 23:01:15 -04:00
Ronan Jouchet
561beda96e Actually actually (TM) include lockfile in npm artifacts
Previous attempt failed by design of `npm pack` / `npm publish`,
as documented at https://docs.npmjs.com/cli/v6/configuring-npm/package-lock-json :

> One key detail about package-lock.json is that it cannot be published,
> and it will be ignored if found in any place other than the toplevel
> package. It shares a format with npm-shrinkwrap.json, which is
> essentially the same file, but allows publication.
>
> This is not recommended unless deploying a CLI tool or otherwise using
> the publication process for producing production packages.

, and we are a CLI tool. Switching to shrinkwrap.
2021-09-24 22:44:07 -04:00
Ronan Jouchet
8fdceee4dc Update changelog for v45.0.3 2021-09-24 22:21:33 -04:00
Ronan Jouchet
904f7d0a57 Actually include package-lock.json in npm artifacts, duh 2021-09-24 22:21:09 -04:00
Ronan Jouchet
94142955f1 Update changelog for v45.0.2 2021-09-24 22:16:59 -04:00
Adam Weeden
24115bb3bd
Fix regressions in opening windows/tabs, update browser versions (#1284)
* Update electron to 13.4.0 + update browser versions

* Fix injectCSS preventing new windows from opening

* Fix some window/tab opening weirdness/bugs

* Fix unit tests

* Switch to using onResponseStarted to avoid the issues with callbacks in the future

* Clear up comments on onResponseStarted
2021-09-24 22:03:03 -04:00
Adam Weeden
d759695e5a
Workaround yargs coerce issue (#1283) 2021-09-22 09:26:49 -04:00
Adam Weeden
46424f9795
Make macOS "bundle identifier" mention Nativefier (fix #866) (#1259)
* Make macos bundle identifier mention nativefier

* Fix @types/debug here too

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-09-21 17:32:55 -04:00
Ronan Jouchet
0d78978d9e Update changelog for v45.0.1 2021-09-20 11:45:36 -04:00
Ronan Jouchet
e222e99b56 Bump default Electron to 13.4.0 2021-09-20 11:38:15 -04:00
Ronan Jouchet
7ab2c14f4f Deps bumps: @types/jest, eslint-plugin-prettier 2021-09-20 11:32:47 -04:00
Ronan Jouchet
0fbe7d39cb Build: re-introduce a package-lock.json file
They were used a long time ago, then I scrapped them for simplicity to
new contributors. I'm re-considering this and re-introducing one, for
two (maybe three) reasons:

1. Reading on supply chain attacks
2. Build broken because of a dep change (see previous commit broken
   because of a change in yargs @ 17.1.0)
(3.) Performance
2021-09-20 11:25:43 -04:00
Ronan Jouchet
fcc3906f52 build:watch script: make it watch project folders 2021-09-20 10:32:32 -04:00
Ronan Jouchet
1a54d286d8 Internal login pages: add (id|auth).atlassian.com (fix #1265) 2021-07-28 23:22:02 -04:00
Ronan Jouchet
33f293120d API.md: document need to use CSS "!important" keyword (fix #1264) 2021-07-28 08:10:31 -04:00
Ronan Jouchet
167f1e3be8 Update changelog for v45.0.0 2021-07-19 13:36:19 -04:00
Adam Weeden
bf4be860cf
Upgrade to Electron 13 (#1230)
* Catch promise errors better

* Move subFunctions to bottom of createNewWindow

* Use parents when creating child BrowserWindow instances

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

* Inject browserWindowOptions better

* Interim refactor to MainWindow object

* Split up the window functions/helpers/events some

* Further separate out window functions + tests

* Add a mock for unit testing functions that access electron

* Add unit tests for onWillPreventUnload

* Improve windowEvents tests

* Add the first test for windowHelpers

* Move WebRequest event handling to node

* insertCSS completely under test

* clearAppData completely under test

* Fix contextMenu require bug

* More tests + fixes

* Fix + add to createNewTab tests

* Convert createMainWindow back to func + work out gremlins

* Move setupWindow away from main since its shared

* Make sure contextMenu is handling promises

* v13.1.2

* v13.1.4

* Update Webkit version for Safari

* 13.1.6 -> NO CRASH!

* Fix types/debug build error on Ubuntu

* 13 -> 13.1.7

* Bump default Firefox version

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-07-19 13:03:46 -04:00
Ronan Jouchet
5d9cb91739 Fix 'npm install'-time warning about outdated companion DT types
```
npm WARN deprecated @types/loglevel@1.6.3: This is a stub types definition. loglevel provides its own type definitions, so you do not need this installed.
npm WARN deprecated @types/electron-packager@15.0.1: This is a stub types definition. electron-packager provides its own type definitions, so you do not need this installed.
```

Also, add `@types/debug` necessary to fix build, as already done by
@TheCleric in https://github.com/nativefier/nativefier/pull/1230/files

Also, for all deps, bump minimum version to current version
2021-07-16 18:55:10 -04:00
Ronan Jouchet
c4327e97a7 Update changelog for v44.0.7 2021-07-10 16:06:47 -04:00
Ronan Jouchet
97b33b369f Bump default Electron from 12.0.12 to 12.0.14
- https://github.com/electron/electron/releases/tag/v12.0.13
- https://github.com/electron/electron/releases/tag/v12.0.14
2021-07-10 15:59:38 -04:00
Omoeba
95e2ff9a3f
App context menu: Add electron-context-menu image entries - Save, Copy, Copy Address (PR #1256) 2021-07-10 10:55:00 -04:00
Sam Potts
5e526fb000
Fix badge/counter icon not being removed correctly (fixes #1251) (PR #1252)
The current check for `count` means the value to reset/remove the badge (`''`) is treated as `false` and therefore the badge does not get removed. This PR changes the check for `undefined` instead which resolves the issue. 

Related issue: 
https://github.com/nativefier/nativefier/issues/1251

Testing performed:
- I ran `npm t` as per the docs. All tests passed. 
- I created a new app build for Twitter (`https://twitter.com`) and Messenger (`https://messenger.com`) to verify the correct behaviour. Both were showed the issue as resolved. 

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-07-05 18:35:50 -04:00
Ronan Jouchet
629369d885 API.md: help users not be confused by platform vs. architecture (fix #1254) 2021-07-05 17:22:44 -04:00
Ronan Jouchet
a9bab01858
HACKING.md: document a few maintainer deps things (#1248) 2021-06-29 00:57:58 -04:00
Ronan Jouchet
9943d7f3cc HACKING.md: formatting & nits 2021-06-28 23:04:49 -04:00
Ronan Jouchet
d0849ce794 generate-changelog: integrate running "npm out" for a reminder of outdated dependencies 2021-06-28 22:56:53 -04:00
Ronan Jouchet
af80acf7e9 Update changelog for v44.0.6 2021-06-26 10:55:11 -04:00
Adam Weeden
b74c0bf959
Make app strict TypeScript + linting (and add a shared project) (#1231)
* Convert app to strict typing + shared library

* Fix new code post-merge

* Remove extraneous lint ignores

* Apply suggestions from code review

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* Fix prettier complaint

* Dedupe eslint files

* Fix some refs after merge

* Fix clean:full command

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-06-26 09:59:28 -04:00
Ronan Jouchet
f8f48d2f09 Update changelog for v44.0.5 2021-06-25 16:17:59 -04:00
Ronan Jouchet
9893d19085 Bump default Electron to 12.0.12 2021-06-25 16:10:12 -04:00
Adam Weeden
a6c1dcf952
Fix "Reset Zoom" menu item (fix #1241) (PR #1243) 2021-06-25 16:05:23 -04:00
Adam Weeden
dc0e6cb68f
Fix app keeping running in the background after closed (--tray false regression) (PR #1242)
By tightening up typing and logic around tray options.
2021-06-25 16:03:23 -04:00
Adam Weeden
d69d4b253a
Fix tray icon always on (regression from #1235) 2021-06-22 19:09:11 -04:00
Adam Weeden
a491e34966
Fix --tray start-in-tray (fix #1225) (#1235) 2021-06-22 01:22:46 -04:00
Adam Weeden
8b34c6d12d
Resolves #1228; Don't treat 'about:blank#blocked' as new tabs (#1229) 2021-06-16 23:03:49 -04:00
Ronan Jouchet
37769e463c HACKING.md: mention app pkgjson/devDeps/electron 2021-06-16 19:16:55 -04:00
Ronan Jouchet
b2af6f5435 HACKING.md: clarification 2021-06-16 19:15:05 -04:00
Ronan Jouchet
564c616d12 HACKING.md: formatting 2021-06-16 19:13:11 -04:00
Ronan Jouchet
05c3cbfb76 HACKING.md: document major-updating Electron 2021-06-16 19:12:31 -04:00
Ronan Jouchet
996183b949 Update changelog for v44.0.4 2021-06-15 22:36:01 -04:00
Adam Weeden
7a08a2d676
Enable TypeScript strict:true, and more typescript-eslint rules (#1223)
* Catch promise errors better

* Move subFunctions to bottom of createNewWindow

* Use parents when creating child BrowserWindow instances

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

* Inject browserWindowOptions better

* Interim refactor to MainWindow object

* Split up the window functions/helpers/events some

* Further separate out window functions + tests

* Add a mock for unit testing functions that access electron

* Add unit tests for onWillPreventUnload

* Improve windowEvents tests

* Add the first test for windowHelpers

* Move WebRequest event handling to node

* insertCSS completely under test

* clearAppData completely under test

* Fix contextMenu require bug

* More tests + fixes

* Fix + add to createNewTab tests

* Convert createMainWindow back to func + work out gremlins

* Move setupWindow away from main since its shared

* Make sure contextMenu is handling promises

* Fix issues with fullscreen not working + menu refactor

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

* Requested PR changes

* Add strict typing; tests currently failing

* Fix failing unit tests

* Add some more eslint warnings and fixes

* More eslint fixes

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

* Fix the package.json import/require

* Fix some funky test errors

* Warn -> Error for eslint rules

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

* Add back the ext code I removed
2021-06-15 22:20:49 -04:00
Ronan Jouchet
7807bbb327 Update changelog for v44.0.3 2021-06-15 09:06:44 -04:00
Adam Weeden
113d8448c1
Fix CSS injection (#1227)
This fixes https://github.com/nativefier/nativefier/pull/1222#issuecomment-860913698 , where:

1. When it works (e.g. initial page load), CSS is slower to inject (you can see pages without injected CSS)
2. CSS isn't injected when navigating to a page

On both Windows & Linux, a `git revert 9a6c6f870d && npm run build` fixes the issue.

--

I'm still not 100% sure what went wrong, but I suspect that the new version of Electron may not be firing onHeadersReceived for the actual navigation events, and only its child requests. To counteract it, I'm now injecting at the navigation event as well. I was able to reproduce the issue and this does seem to fix it. Please let me know if it does for you as well..

Also I noticed some funkiness in your logs where we're trying to inject on font files. So I realized the method is probably not nearly as important as the content-type, so I've switched blacklist methods to blacklist content-types.
2021-06-15 09:02:57 -04:00
Ronan Jouchet
9d0a2f5b44 Bump default Electron to 12.0.11 2021-06-14 14:39:51 -04:00
Ronan Jouchet
ea24a0fdeb Fix gitcloud 0.2.3 import 2021-06-14 14:39:43 -04:00
Adam Weeden
9a6c6f870d
Selective css injection (#1222)
* Don't inject CSS for unneeded responses

* Remove some non-injectable methods

* Actually check for CSS to inject + unit tests

* Update app/src/helpers/windowHelpers.ts

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-06-09 09:49:35 -04:00
Peter Lewis
d67f533fa5
README: Update node & npm requirements (#1224)
Update required `node` and `npm` versions to match what's in package.json
2021-06-09 08:14:46 -04:00
Ronan Jouchet
437ae55d2e Update changelog for v44.0.2 2021-06-07 17:28:34 -04:00
Ronan Jouchet
4badf3dc70 manual-test: fix shellcheck nits, make tests structure more distinct 2021-06-07 17:27:23 -04:00
Adam Weeden
826625f4a4
Fix HTTP basic auth (fix #1219) (#1220)
Should resolve #1219, as well adds a manual test for basic auth as suggested.

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-06-07 17:09:24 -04:00
Adam Weeden
363fa966ee
Fix tabs opening twice (fix #1209) (#1221)
* Only add a new window handler to the main window

* Update app/src/components/mainWindow.ts

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-06-07 16:49:06 -04:00
Ronan Jouchet
460d70f915 Update changelog for v44.0.1 2021-06-07 09:38:53 -04:00
Ronan Jouchet
709557c7ab
macOS: fix crash on activating main window (fix #1212) (PR #1213)
```
Uncaught Exception:
TypeError: Cannot read property 'show' of undefined
at App.<anonymous> (/src/main.ts:177:18)
```

Seems like an oversight of the recent refactor.

* macOS: fix crash on activating main window (fix #1210)
* Eliminate need for global mainWindow, refactor widevine / non-widevine events (#1218)

Co-authored-by: Adam Weeden <adamweeden@gmail.com>
2021-06-07 09:35:27 -04:00
Adam Weeden
cdc6fa79c2
Fix fullscreen not working + menu refactor (fix #1206) (#1210)
* Catch promise errors better

* Move subFunctions to bottom of createNewWindow

* Use parents when creating child BrowserWindow instances

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

* Inject browserWindowOptions better

* Interim refactor to MainWindow object

* Split up the window functions/helpers/events some

* Further separate out window functions + tests

* Add a mock for unit testing functions that access electron

* Add unit tests for onWillPreventUnload

* Improve windowEvents tests

* Add the first test for windowHelpers

* Move WebRequest event handling to node

* insertCSS completely under test

* clearAppData completely under test

* Fix contextMenu require bug

* More tests + fixes

* Fix + add to createNewTab tests

* Convert createMainWindow back to func + work out gremlins

* Move setupWindow away from main since its shared

* Make sure contextMenu is handling promises

* Fix issues with fullscreen not working + menu refactor

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

* Requested PR changes
2021-06-07 08:55:17 -04:00
Ronan Jouchet
16cacb0915 CATALOG.md: Spotify: mention faking user agent may not be necessary 2021-06-04 18:53:38 -04:00
Ronan Jouchet
69bc3306f9 Update changelog for v44.0.0 2021-06-04 17:03:35 -04:00
Ronan Jouchet
5d67156deb API.md: cleanup 2021-06-04 16:53:35 -04:00
Ronan Jouchet
c6b50e8d0a API.md: add "New in x.y.z" info for (most) flags 2021-06-04 16:45:05 -04:00
Ronan Jouchet
5673989117 Bump default Electron to 12.0.10 2021-06-04 16:18:34 -04:00
Adam Weeden
72de7b3fca
Refactor app window creation/events + add some unit tests; fix #1197 (#1203)
* Catch promise errors better

* Move subFunctions to bottom of createNewWindow

* Use parents when creating child BrowserWindow instances

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

* Inject browserWindowOptions better

* Interim refactor to MainWindow object

* Split up the window functions/helpers/events some

* Further separate out window functions + tests

* Add a mock for unit testing functions that access electron

* Add unit tests for onWillPreventUnload

* Improve windowEvents tests

* Add the first test for windowHelpers

* Move WebRequest event handling to node

* insertCSS completely under test

* clearAppData completely under test

* Fix contextMenu require bug

* More tests + fixes

* Fix + add to createNewTab tests

* Convert createMainWindow back to func + work out gremlins

* Move setupWindow away from main since its shared

* Make sure contextMenu is handling promises
2021-06-02 15:18:32 -04:00
Ronan Jouchet
ec12702359 HACKING.md: video doesn't embed, just link to it 2021-06-02 09:52:28 -04:00
Ronan Jouchet
1ceffb1a0d HACKING.md: embed video showing what a live-reload experience looks like
Also, a paragraph on limiting breaking changes
2021-06-02 09:48:57 -04:00
Ronan Jouchet
62ee24662c More test:watch fix: bring upgrade to Jest 27 from TheCleric' closed jest-ts PR
See https://github.com/nativefier/nativefier/pull/1204
2021-06-02 00:35:19 -04:00
Ronan Jouchet
6b09d1467f More test:watch fixes & usability
See https://github.com/nativefier/nativefier/pull/1204#issuecomment-852679403
2021-06-01 23:37:04 -04:00
Ronan Jouchet
9c5dba7f07 Fix test:watch requiring two saves to actually run the code/test you just changed
Will fix https://github.com/nativefier/nativefier/pull/1204#issuecomment-852155755
See package.json in comments for description
2021-06-01 20:43:46 -04:00
Adam Weeden
86a27d4f39
Allow non-ascii app names like 微信读书 (fix #1056) (#1207)
* Resolves #1056 Allow non-ascii app names like 微信读书

* Update src/utils/sanitizeFilename.ts

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* Fix prettier

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-05-31 23:27:32 -04:00
Adam Weeden
6c55e1a9a1
Add login.microsoftonline.com to internal login pages (#1205)
* Add login.microsoftonline.com to internal login pages

* Update API.md

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* Add extra messaging for adding to internalLoginPages

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-05-29 20:38:24 -04:00
Ronan Jouchet
8801ca5150 app/tsconfig.json: document when to bump compilerOptions, and rationale 2021-05-29 20:07:38 -04:00
Ronan Jouchet
2b780e6c67 Test watcher: warn that it's necessary to run build:watch for the test watcher to work
See https://github.com/nativefier/nativefier/pull/1204#issuecomment-850915981
2021-05-29 19:58:46 -04:00
Adam Weeden
d6730f7022
Improve user agent handling/provide user agent "short" codes (#1198) 2021-05-21 23:41:13 -04:00
Ronan Jouchet
4f3b449218 Deps: bump ts-loader from 8 to 9, now that we require Node 12
See https://github.com/TypeStrong/ts-loader/releases
2021-05-21 19:44:53 -04:00
Ronan Jouchet
45db4e7ec6 README: whoopsie, bad rename of animation 2021-05-21 18:17:59 -04:00
Ronan Jouchet
cc02b87de7
Get rid of "docs" folder (#1194)
It contains a weird mix of stuff and hides valuable files from view at the root of the repo. Better to have:
- Docs at the root
- Rest of the github/release-related hodgepodge (screenshots, scripts) in hidden folder .github
2021-05-21 18:16:59 -04:00
Adam Weeden
1a810e5ce5
Organize CLI flags into groups (for better --help usability) (#1191)
* Organize CLI options for better UX

* Fix some documentation

* Whoops. Stupid VS Code linter.

* Fix prettier issues

* Make paths less unixy in tests

* Update src/cli.test.ts

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* Apply suggestions from code review

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* Add example to reference CATALOG.md

* Make honest appear near user-agent

* Standardize descriptions

* Hide flash options

* Add explanation of parsed._

* Redo groups in yargs

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-05-18 22:02:55 -04:00
Adam Weeden
b3c202fd33
Bump minimum required version: node>=12.9, npm>=6.9 (#1192)
* Move minimum supported version: node=12, npm=6.9

* Add missing bits and documentation for future bumping

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-05-15 13:32:54 -04:00
Ronan Jouchet
9225114e77 Update changelog for v43.1.3 2021-05-15 13:28:16 -04:00
Ronan Jouchet
3fd34549d6 Bump to Electron 12.0.7 2021-05-15 13:12:34 -04:00
Ronan Jouchet
0216d4f9c4 CATALOG.md: add HBO Max
From https://github.com/nativefier/nativefier/issues/1153#issuecomment-840697278
2021-05-13 22:51:08 -04:00
Knallli
668ca723dc
CATALOG.md: Add Spotify (PR #1187)
Co-authored-by: Knallli <41125802+Knallli@users.noreply.github.com>
Co-authored-by: Adam Weeden <adamweeden@gmail.com>
Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-05-12 19:53:47 -04:00
Ronan Jouchet
b9615b4cbd
CATALOG.md: cleanup & add Google apps section (#1188) 2021-05-12 18:39:52 -04:00
Ronan Jouchet
d61a4e709a Make prettier happier 2021-05-10 10:23:56 -04:00
Ronan Jouchet
03325b45ce README: remove broken npm badge, and btw useless build status 2021-05-10 10:11:34 -04:00
Adam Weeden
5fbf14d35d
Only try to inject CSS for valid web requests (fix #939: crash on tab close, PR #1181) 2021-05-08 10:12:10 -04:00
RippiN
067327e863
linkIsInternal: consider shop.foo.com and blog.foo.com as internal (still without extra dep or SLD list) (PR #1171)
Domain-ish URL test now also covers considering internal URLs with different sub-domains like
- `https://listen.tidal.com`
- `https://login.tidal.com`

Co-authored-by: Daniel Fuchs <dfuchs@multamedio.de>
Co-authored-by: Adam Weeden <adamweeden@gmail.com>
Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-05-03 13:22:44 -04:00
deciacco
d6f0a28a90
CATALOG.md: build command library (fix #1166) (PR #1178) 2021-05-03 13:08:32 -04:00
Ronan Jouchet
6cf533c5ac Update changelog for v43.1.2 2021-05-03 11:19:30 -04:00
Ronan Jouchet
331fe8db39
Fix logging out users on upgrade / app recreate with same URL (fix #1176) (PR #1179)
This is a follow-up of https://github.com/nativefier/nativefier/pull/1162#discussion_r623766247

PR #1162 introduced a new `generateRandomSuffix` helper function,
used it for its needs (avoiding collisions of injected js/css).

But it also replaced existing appname normalizing logic with it,
introducing randomness in a place that used to be deterministic.

As a result, starting with dd6e15fb5 / v43.1.0, re-creating an app would cause
the app to use a different appName, thus a different appData folder, thus
losing user data including cookies.

This PR leaves the `--inject` fixes of #1176, but reverts the appName logic
to the pre-#1176 code.
2021-05-03 11:16:50 -04:00
Ronan Jouchet
beae6e872b development.md: expand on why I'm such a bore with limiting extra deps 2021-05-02 18:29:05 -04:00
Ronan Jouchet
d9670d6bb0 Update changelog for v43.1.1 2021-05-02 18:15:08 -04:00
Adam Weeden
14d95da079
Fix crash in preload.js due to 3rd-party 'loglevel' (fix #1175, fix #1176) (PR #1177)
Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-05-02 18:12:08 -04:00
Ronan Jouchet
e58823985a Update changelog for v43.1.0 2021-05-01 11:09:05 -04:00
Ronan Jouchet
36a9e2d616 Bump to Electron 12.0.6 with security fixes
See https://github.com/electron/electron/releases/tag/v12.0.6
2021-05-01 10:48:29 -04:00
Adam Weeden
2774888924
App: fix child windows not inheriting mainWindow properties, including userAgent (#1174)
When a new child window is spawned (such as for a Google login popup), those child windows were not receiving the mainWindow's properties. Chiefly among this was the userAgent which caused a bug in #831
2021-05-01 10:46:40 -04:00
Adam Weeden
bcdbd58f06
App: replace console.xyz calls with loglevel.xyz, with a level controlled by app argv --verbose (#1172)
In reference to request in https://github.com/nativefier/nativefier/pull/1168/files#r623753290 ,
this PR fixes a lot of the disparity in logging in the app, and fleshes the logging out a bit.
2021-04-30 23:21:37 -04:00
Adam Weeden
895c11a53e
Support setting the browserWindow's language (fix #175) (PR #1173 ) 2021-04-30 23:16:57 -04:00
Adam Weeden
7dc189ef3f
Support creating self-contained "portable" apps writing their app data to the app folder (fix #376) (PR #1168)
In response to #376

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-04-30 23:10:12 -04:00
Adam Weeden
dd6e15fb5c
Fix injecting multiple css/js files (fix #458) (#1162)
* Add ability to inject multiple css/js files

* API doc: Move misplaced macOS shortcuts doc (PR #1158)

When I added this documentation originally, I guess I placed it in the wrong location.

* README: use quotes in example, to divert users from shell globbing pitfalls

Follow-up of https://github.com/nativefier/nativefier/issues/1159#issuecomment-827184112

* Support opening URLs passed as arg to Nativefied application (fix #405) (PR #1154)

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* macOS: Fix crash when using --tray (fix #527), and invisible icon (fix #942, fix #668) (#1156)

This fixes:

1. A startup crash on macOS when using the `--tray` option; see #527.
  ![image](https://user-images.githubusercontent.com/22625791/115987741-99544600-a5b6-11eb-866a-dadb5640eecb.png)
2. Invisible tray icon on macOS; see #942 and #668.  
   ![image](https://user-images.githubusercontent.com/22625791/115988276-24364000-a5b9-11eb-80c3-561a8a646754.png)

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* API.md / --widevine: document signing apps to make some sites like HBO Max & Udemy work (fix #1147)

* Prompt to confirm when page is attempting to prevent unload (#1163)

Should alleviate part of the issue in #1151

* Add an option to upgrade an existing app (fix #1131) (PR #1138)

This adds a `--upgrade` option to upgrade-in-place an old app, re-using its options it can.
Should help fix #1131

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>

* Bump to Electron 12.0.5 with Chrome 89.0.4389.128

* Add newly discovered Google internal login page (#1167)

* Fix Widevine by properly listening to widevine-... events, and update docs (fix #1153) (PR #1164)

As documented in #1153, for Widevine support to work properly, we need to listen for the Widevine ready event, and as well for certain sites the app must be signed.

This PR adds the events, and as well adds better documentation on the signing limitation.

This may also help resolve #1147

* Improve suffix creation + tests

* API: clarif in existing doc by the way

* Typo

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
Co-authored-by: Ben Curtis <github@nosolutions.com>
Co-authored-by: Fabian Wolf <22625791+fabiwlf@users.noreply.github.com>
2021-04-30 11:04:10 -04:00
Ronan Jouchet
1ad1844619 linkIsInternal: add test to ensure we don't regress on cases of SLDs 2021-04-30 07:52:58 -04:00
Ronan Jouchet
c4356e48f1
Fix app not starting since widevine PR / #1164 (#1170) 2021-04-30 01:23:13 -04:00
Adam Weeden
ec0ea4bd67
README: add troubleshooting section for common issues (#1169)
Per #1112, I'm adding some common troubleshooting steps to the README. As well, this adds a checkbox requesting users to check this section before submitting a ticket, which will hopefully lead to more self-help.
2021-04-29 20:02:43 -04:00
Adam Weeden
f6e1ebd876
Fix Widevine by properly listening to widevine-... events, and update docs (fix #1153) (PR #1164)
As documented in #1153, for Widevine support to work properly, we need to listen for the Widevine ready event, and as well for certain sites the app must be signed.

This PR adds the events, and as well adds better documentation on the signing limitation.

This may also help resolve #1147
2021-04-29 13:27:55 -04:00
Adam Weeden
d2c7de37a2
Add newly discovered Google internal login page (#1167) 2021-04-29 13:24:15 -04:00
Ronan Jouchet
a19ccd5fda Bump to Electron 12.0.5 with Chrome 89.0.4389.128 2021-04-28 22:05:17 -04:00
Adam Weeden
9330c8434f
Add an option to upgrade an existing app (fix #1131) (PR #1138)
This adds a `--upgrade` option to upgrade-in-place an old app, re-using its options it can.
Should help fix #1131

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-04-28 22:00:31 -04:00
Adam Weeden
bc6be8445d
Prompt to confirm when page is attempting to prevent unload (#1163)
Should alleviate part of the issue in #1151
2021-04-28 21:37:50 -04:00
Ronan Jouchet
83b284e727 API.md / --widevine: document signing apps to make some sites like HBO Max & Udemy work (fix #1147) 2021-04-28 21:09:25 -04:00
Fabian Wolf
cf8e51e7ab
macOS: Fix crash when using --tray (fix #527), and invisible icon (fix #942, fix #668) (#1156)
This fixes:

1. A startup crash on macOS when using the `--tray` option; see #527.
  ![image](https://user-images.githubusercontent.com/22625791/115987741-99544600-a5b6-11eb-866a-dadb5640eecb.png)
2. Invisible tray icon on macOS; see #942 and #668.  
   ![image](https://user-images.githubusercontent.com/22625791/115988276-24364000-a5b9-11eb-80c3-561a8a646754.png)

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-04-28 20:25:15 -04:00
Ben Curtis
41b208fcb7
Support opening URLs passed as arg to Nativefied application (fix #405) (PR #1154)
Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-04-28 20:18:12 -04:00
Ronan Jouchet
9286fc8086
README: use quotes in example, to divert users from shell globbing pitfalls
Follow-up of https://github.com/nativefier/nativefier/issues/1159#issuecomment-827184112
2021-04-27 18:12:02 -04:00
Adam Weeden
12c8c9f4cb
API doc: Move misplaced macOS shortcuts doc (PR #1158)
When I added this documentation originally, I guess I placed it in the wrong location.
2021-04-26 12:25:47 -04:00
Ronan Jouchet
9f561c0091 CI: run less node versions, oldest supported / latest is enough 2021-04-23 21:52:03 -04:00
Ronan Jouchet
09accbd28a CI: run in node 16, stop node 15 2021-04-23 21:47:54 -04:00
Chris Dzombak
b4ddd6865c
Support defining a custom bookmarks menu (fix #1065) (PR #1155)
This PR adds an optional, customizable menu of predefined bookmarks. In addition to containing a list of bookmarks, this file customizes the name of the menu and (optionally) allows assigning keyboard shortcuts to bookmarks. It adds a new command-line flag, `--bookmarks-menu <string>`, which can be set as the path to a JSON file containing configuration for the bookmarks menu.

Example of such a JSON file:

```json
{
    "menuLabel": "Music",
    "bookmarks": [
        {
            "title": "lofi.cafe",
            "url": "https://lofi.cafe/",
            "type": "link",
            "shortcut": "Cmd+1"
        },
        {
            "title": "beats to relax/study to",
            "url": "https://www.youtube.com/watch?v=5qap5aO4i9A",
            "type": "link",
            "shortcut": "Cmd+2"
        },
        {
            "type": "separator"
        },
        {
            "title": "RÜFÜS DU SOL Live from Joshua Tree",
            "type": "link",
            "url": "https://www.youtube.com/watch?v=Zy4KtD98S2c"
        }
    ]
}
```

## Checks
- [x] `npm run ci` passes

## Notes

Compared to the fork linked in #1065, this PR:
- adds no dependencies
- doesn't currently support submenus (this should be easy enough to add, but I didn't need it)

## Screenshot

<img width="853" alt="screenshot" src="https://user-images.githubusercontent.com/102904/115882015-5493a800-a41a-11eb-85ef-a190f3dbfe76.png">
2021-04-23 21:46:34 -04:00
Ronan Jouchet
fa9bd2aba5 Update changelog for v43.0.2 2021-04-13 21:53:05 -04:00
Ronan Jouchet
83b1c10269 Bump to Electron 12.0.4 with Chrome 89.0.4389.114
With recent security fixes.
See https://github.com/electron/electron/releases/tag/v12.0.4
and https://github.com/electron/electron/releases/tag/v12.0.3
2021-04-13 21:49:31 -04:00
Ronan Jouchet
f64c05f735 Update changelog for v43.0.1 2021-04-11 20:58:20 -04:00
Ronan Jouchet
ecfb5e92bb Old build detection: doc 2021-04-11 20:54:44 -04:00
Ronan Jouchet
1474ebc8bc Bump to Electron 12.0.2 with Chrome 89.0.4389.90
See https://github.com/electron/electron/releases/tag/v12.0.2
2021-04-11 20:47:16 -04:00
Ronan Jouchet
da06d933df Old build detection: bump to 90 days, let packagers customize message 2021-04-11 20:40:41 -04:00
Alec Mev
d0ab749bd6
Add Apple ID to automatically-internal login pages (#1146)
Used by Notion, for example. TLDs other than `.com` don't appear to exist.
2021-04-01 18:03:07 -04:00
Evan Anderson
4298d2da06
Automatically-internal login URLs: add GitHub 2FA pages (PR #1140)
I have 2FA (FIDO/Yubikey) set up for GitHub, and the session login was redirecting to my browser. Looking at the redirect path, it appears that github.com/session is involved, so adding that to internal login details.

With this patched, I'm able to login in to https://octobox.io/ in nativefier.
2021-03-17 07:39:24 -04:00
Adam Weeden
50ce2f81dd
Add a session-interaction event to allow injected js to interact with apps Electron session object (PR #1132)
As discussed in #283 this PR will allow injected JS to do SOME interaction with the Electron session.

There is a full explanation of what this feature can, and cannot do, with examples in the api.md documentation.

This will provide a path for resolving many of our issues where users may "self-service" the solution by injecting JS that performs the task needed to meet their objectives.

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-03-13 21:24:48 -05:00
Ronan Jouchet
da007305de dev.md: typo 2021-03-11 20:28:28 -05:00
Ronan Jouchet
3c2e84a752
Dev.md: guidelines (PR #1133)
An attempt to a few important things to newcomers, to reduce the risk
of contributors being disappointed / contradicted late at PR time.
2021-03-11 20:27:21 -05:00
Ronan Jouchet
eaa9d8471b Old build detection: simpler error message 2021-03-11 09:37:33 -05:00
Ronan Jouchet
554c70c12d API.md: document Flash deprecation 2021-03-10 23:02:51 -05:00
Ronan Jouchet
96f3ab4ec7 Update changelog for v43.0.0 2021-03-10 20:37:37 -05:00
Ronan Jouchet
dcadfb0bca Bump to Electron 12.0.1 with Chrome 89.0.4389.82 2021-03-10 20:25:33 -05:00
Ronan Jouchet
1521a28f45 Docker: fix build 2021-03-10 20:06:39 -05:00
Ronan Jouchet
12373b620e Doc/API: update outdated --internal-urls "second-level-domain" stuff
See https://github.com/nativefier/nativefier/pull/1126
and c0a6604676
2021-03-10 19:49:22 -05:00
Ronan Jouchet
ffa421eb8e Docker: attempt to fix build
See https://hub.docker.com/api/audit/v1/action/05620acd-9984-4e10-9053-3a7edc0c8558/ :

```
The command '/bin/sh -c npm link && npm test && rm -rf /tmp/nativefier* ~/.npm/_cacache ~/.cache/electron && chmod +x $NPM_PACKAGES/bin/nativefier' returned a non-zero code: 243
```

Culprit could be `rm -rf /tmp/nativefier*` : at this point /tmp/nativefier
doesn't exist, so `sh` cannot evaluate glob `/tmp/nativefier*`, and exits 1
2021-03-10 19:44:44 -05:00
Ronan Jouchet
21665cac5f Bump deps 2021-03-10 19:41:02 -05:00
Ronan Jouchet
74a7d3375d App: revert addition of extra flag --internal-login-pages
See discussion at https://github.com/nativefier/nativefier/pull/1124#issuecomment-794751514 :

> @TheCleric I was about to merge this, then reconsidered one little thing (yes I wrote "little", I'm not reconsidering this whole thing 😅).
>
> I'm re-considering having the extra flag. I'm not so sure this will harm a lot of use cases. I'd like to 1. merge this PR, 2. immediately follow up with a small commit removing the flag & adjusting api.md, 3. release with the change well-documented / asking for feedback if this is problematic to anyone. (I'm not asking you any extra work, and like leaving an in-tree commit trace of considering the flag). If people complain with a valid reason, we'll restore the flag with a quick revert, else we're happy with one less flag and a reasonably-handled breaking change.
>
> Thoughts / objections?

Answered by:

> That seems reasonable to me.
>
> [discussion on an extra structured way to pass flags]
2021-03-10 19:36:20 -05:00
Adam Weeden
6f7e80bafd
App: Automatically consider known login pages as internal (fix #706) (PR #1124)
Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-03-10 19:20:53 -05:00
Adam Weeden
e9ccb35825
Docker: slim down image size, by removing temp/cache files (PR #1128)
Per the [note](https://github.com/nativefier/nativefier/pull/1122#discussion_r588922780) by @SuperSandro2000 in #1122 Docker will still cache files in intermediate layers if you delete them, so they'll still be part of the image.

Only solution seems to be to delete them as you create them so they don't cache:
Per https://stackoverflow.com/questions/53998310/docker-remove-file-from-intermediate-layer

Co-authored-by: Sandro <sandro.jaeckel@gmail.com>
2021-03-10 18:54:33 -05:00
Ronan Jouchet
9b455670c4 generate-changelog: integrate doc, creating tag, remind about smoke test 2021-03-04 21:27:13 -05:00
Ronan Jouchet
7ee2f97599 Update changelog for v42.4.0 2021-03-04 12:46:10 -05:00
Ronan Jouchet
e59e056f59 Bump deps 2021-03-04 12:37:11 -05:00
Ronan Jouchet
c0a6604676
Fix considering "same domain-ish" URLs as internal (PR #1126)
In 6b266b7815, as I got rid of deprecated dep `wurl`, I wrote:

> This one may be problematic, as it used to do TLD stuff:
> https://github.com/websanova/node-url/blob/7982a613bc/wurl.js#L4
>
> So, the new WHATWG-URL-based implementation will consider
> `asana.com` to be "external" to `app.asana.com`, contrarily to before.
> Given the nature of Nativefier, I think it's actually what to expect,
> that in this case you're "out of the app", and in e.g. asana's landing
> page, which you'd expect to see in your browser.

Turns out it's even more problematic: @TheCleric notices in https://github.com/nativefier/nativefier/pull/1124#issuecomment-790279403
that this breaks app `https://evernote.com` doing its login in `www.evernote.com`

The present change fixes this, by behaving mostly similarly to before,
but without re-introducing `wurl` or another dep needing a TLD/SLD list.
2021-03-04 10:00:53 -05:00
Adam Weeden
8f9135312b
Docker: fix Windows builds (fix #997), line endings, switch to Alpine (PR #1122)
- Docker builds for Windows are fixed (fixes #997)
- Switched over to use Alpine (as was indicated as desired in https://github.com/nativefier/nativefier/issues/375#issuecomment-304247033) - which may mean #375 is fixed as well.
- Fixed bug where Docker has the wrong line endings when copying from a Windows host
- Fixed the invalid `arm` arch to `armv7l`
- Add `npm t` to the docker build to ensure tests pass before we start trying to do builds
- Add a message to help the user when trying to build Mac apps on Windows as a non-Admin (currently an unhelpful exception)

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-03-02 00:16:30 -05:00
C. Mangla
cbb4380583
Icon conversion: support GraphicsMagick in addition to ImageMagick (PR #1002)
Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-02-28 14:21:28 -05:00
Adam Weeden
adcf21a3df
macOS: Prompt for accessibility permissions if needed by Global Shortcuts using Media Keys (Fix #1120) (PR #1121)
When setting a media key (play, pause, next/previous track) as global shortcut in Mac OS 10.14+, accessibility permissions must be given to the app for it to work (see https://www.electronjs.org/docs/api/global-shortcut?q=MediaPlayPause#globalshortcutregisteraccelerator-callback).

This PR will accomplish the following on generated app launch:
- Check if global shortcuts are being setup
- Check if the host OS is Mac OS
- Check if the global shortcuts were one of the media keys
- If the above are true, check if the app has accessibility permissions
- If the app does not have the accessibility permissions it will ask the user if they would like to be prompted for these permissions, and then ask Mac OS to prompt for accessibility permissions.

~~As well, a new command line flag is added (`--no-accessibility-prompt`) to preventatively suppress these prompts if desired.~~

Screenshots of the new behavior:
![Screen Shot 2021-02-26 at 2 41 21 PM](https://user-images.githubusercontent.com/547567/109356260-76bfde00-784e-11eb-8c36-3a51b911b780.png)
![Screen Shot 2021-02-26 at 2 41 28 PM](https://user-images.githubusercontent.com/547567/109356266-79223800-784e-11eb-94eb-66437c05fd10.png)
![Screen Shot 2021-02-26 at 2 41 50 PM](https://user-images.githubusercontent.com/547567/109356270-7aebfb80-784e-11eb-9e90-e09bb49752c6.png)

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-02-28 10:24:14 -05:00
Ronan Jouchet
75aa10382b Move some tooling config (eslintignore, prettierrc) to package.json
To limit amount of dotfiles at repo root
2021-02-27 22:31:59 -05:00
Ronan Jouchet
5129dbafe1 Deps: bump gitcloud to 0.2 and page-icon to 0.4 2021-02-27 01:15:26 -05:00
Ronan Jouchet
519e65e7da TSConfig: bump to target/lib es2018, since we require node10 2021-02-26 22:50:59 -05:00
Ronan Jouchet
d90d9f3d7f Scripts: get rid of dev-up and dev-up-win scripts
They used to be necessary when we did OS-specific stuff in here, but
we're no longer, and `foo && bar` is supported by both *nixes and Windows
2021-02-26 22:41:00 -05:00
Ronan Jouchet
292ac39328 CI: avoid npm funding messages 2021-02-26 22:19:45 -05:00
Ronan Jouchet
e03e07e4bd Speed up CI by avoiding repeated npm install & build already done with "prepare" hook 2021-02-26 22:15:56 -05:00
Ronan Jouchet
0aa8276922 Update changelog for v42.3.0 2021-02-25 19:49:22 -05:00
Ronan Jouchet
17231d707f Bump eslint-config-prettier from ^7.2.0 to ^8.1.0 2021-02-25 19:10:11 -05:00
Ronan Jouchet
1a5d79ecf8 Bump commander from ^4.1.1 to ^7.1.0
Looked at https://github.com/tj/commander.js/blob/master/CHANGELOG.md
and tested a bit, seems like no change is needed
2021-02-25 19:08:37 -05:00
Ronan Jouchet
6b266b7815 (Attempt to) get rid of deprecated app dep wurl
This one may be problematic, as it used to do TLD stuff:
https://github.com/websanova/node-url/blob/7982a613bc/wurl.js#L4

So, the new WHATWG-URL-based implementation will consider
`asana.com` to be "external" to `app.asana.com`, contrarily to before.
Given the nature of Nativefier, I think it's actually what to expect,
that in this case your "out of the app", and in e.g. asana landing's page,
which you'd expect to see in your browser.

Let's see if users disagree with that.
2021-02-25 18:55:28 -05:00
Ronan Jouchet
fe79fd622d Doc extra electron update stuff 2021-02-25 18:46:27 -05:00
Ronan Jouchet
f4a0479a11 Bump default Electron to 11.3.0 with the icon fix (with Chromium 87.0.4280.141) 2021-02-25 18:28:29 -05:00
Ronan Jouchet
5ea4638aea Fix lint 2021-02-25 18:19:30 -05:00
Ronan Jouchet
9b52f210db Get rid of dependency "shelljs"
We don't need a fancy _"portable (Windows/Linux/macOS) implementation
of Unix shell commands on top of the Node.js API"_, we just want to run
a simple script. Replacing with using stdlib `child_process.spawnSync`.
2021-02-25 18:15:24 -05:00
Ronan Jouchet
4bf0226da0 Deps: come back to semver ^a.b.c syntax
Thinking about it again, the user-friendlier `a.x` syntax has one disadvantage
over `^a.b.c`: it doesn't force deps upgrades when they upgrade Nativefier.
`a.x` is fine on initial install, but a user with an insecure dep
(e.g. axios 0.19.0) will _not_ get fixed axios 0.21.1 on upgrading Nativefier.
-> Come back to `a.x` everywhere.

Still not introducing package locks, they're too confusing to new devs.
See https://github.com/nativefier/nativefier/pull/1099#issuecomment-761250232
2021-02-25 08:15:39 -05:00
Ronan Jouchet
b99b2f9632 Deps: bump axios from 0.x to ^0.21.1, to reassure dependabot 2021-02-24 23:12:21 -05:00
Arseny
380c98b23d
API.md: fix typo in option "-v" (#1114) 2021-02-16 08:02:21 -05:00
Jia Hao
7a3730e5a9 Update changelog for v42.2.1 2021-01-30 05:04:51 +00:00
Jia Hao
6316d23762 Move to nativefier organization 2021-01-30 04:49:52 +00:00
Jia Hao
66ff02584e Temporarily increase timeout for network call in test 2021-01-30 04:49:36 +00:00
Milo
9c784dcfaf
Move TS @types from dependencies to devDependencies (PR #1102)
Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-01-24 20:36:06 -05:00
Ronan Jouchet
dcefe0074d Update changelog for v42.2.0 2021-01-18 09:24:28 -05:00
Ronan Jouchet
10f7fed290 Revert default Electron back to 11.1.1 (Chrome 87.0.4280.88) (fix #1101)
Not doing anything more complicated (adding macOS-specific code
or adding code always passing an icon), let's instead wait for
Electron to fix the issue.
2021-01-18 09:18:43 -05:00
Ronan Jouchet
f4a7266783 README: more tweaking 2021-01-16 10:19:29 -05:00
Ronan Jouchet
ef43bb6984 README: wording, fix broken links 2021-01-16 10:18:17 -05:00
Ronan Jouchet
fc35b00f88 Make maintenance status (in /releases until now) more visible by putting it at the top of our README 2021-01-16 10:15:51 -05:00
Ronan Jouchet
422f72aa3b Update changelog for v42.1.0 2021-01-16 08:47:17 -05:00
Ronan Jouchet
dc353bebaf More filename & appname sanitization 2021-01-16 08:40:04 -05:00
Ronan Jouchet
3b28dc46cc Bump default Electron to 11.2.0 (with Chromium 87.0.4280.141) 2021-01-16 08:18:33 -05:00
Ronan Jouchet
b0a953eb2d Get rid of cheerio
Not sure this one will stick, maybe my regex is too naive.
Works for common websites. Let's see
2021-01-15 22:15:48 -05:00
Ronan Jouchet
35d926b959 Fix inferIcon error surfacing in full since recent axios 2021-01-15 21:57:19 -05:00
Ronan Jouchet
17f688de63 Get rid of lodash 2021-01-15 21:50:07 -05:00
Ronan Jouchet
0eaf72ced8 Publish TS types, for them to show up in npm
See https://github.blog/changelog/2020-12-16-npm-displays-packages-with-bundled-typescript-declarations/
2020-12-17 20:59:04 -05:00
Ronan Jouchet
cf11a71a7c Update changelog for v42.0.2 2020-12-07 16:51:49 -05:00
Ronan Jouchet
7fd0c748ba Fix arg validation regression in #1080 with --{x,y} (fix #1084) 2020-12-07 16:50:59 -05:00
122 changed files with 20052 additions and 3356 deletions

View File

@ -1,3 +1,6 @@
# git
.git*
# OSX # OSX
.DS_Store .DS_Store
@ -7,11 +10,12 @@
lib/* lib/*
app/lib/* app/lib/*
built-tests built-tests
# commit a placeholder to keep the app/lib directory
!app/lib/.placeholder
dist dist
app/dist
# Docs
docs
*.md
# Logs # Logs
logs logs
@ -41,9 +45,12 @@ build/Release
# Dependency directory # Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules node_modules
app/node_modules
# IntelliJ project files # IntelliJ project files
.idea .idea
*.iml *.iml
out out
gen gen
.vscode

1
.env Normal file
View File

@ -0,0 +1 @@
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1

View File

@ -1,6 +0,0 @@
node_modules/**
app/node_modules/**
app/lib/**
lib/**
built-tests/**
coverage/**

View File

@ -1,27 +0,0 @@
// # 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 `any`s and making tsc strict
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
},
};

View File

@ -13,16 +13,13 @@ Please include the following in your new issue:
## Pull Requests ## Pull Requests
See [here](https://github.com/jiahaog/nativefier#development) for instructions on how to set up a development environment. See [here](https://github.com/nativefier/nativefier/blob/master/HACKING.md) for instructions on how to set up a development environment.
We follow the [Airbnb Style Guide](https://github.com/airbnb/javascript), please make sure tests and lints pass when you submit your pull request. We follow the [Airbnb Style Guide](https://github.com/airbnb/javascript), please make sure tests and lints pass when you submit your pull request.
The following commands might be helpful: The following commands might be helpful:
```bash ```bash
# Run specs and lint
npm run ci
# Run specs only # Run specs only
npm run test npm run test

93
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -0,0 +1,93 @@
name: Bug Report
description: File a bug report
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report 🙂! Help us help you, **fill this form thoroughly**. An incomplete bug report is a useless bug report.
- type: checkboxes
id: homework
attributes:
label: Homework
options:
- label: I took the time to write a good, descriptive issue title
required: true
- label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md).
required: true
- label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds.
required: true
- label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my bug is new.
required: true
- label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases).
required: true
- type: input
id: nativefier-command
attributes:
label: Nativefier command
description: "Your ***full*** nativefier command, on a ***public*** site."
placeholder: nativefier --verbose --some-option https://mysite.com
validations:
required: true
- type: textarea
id: steps-to-repro
attributes:
label: Steps to reproduce
placeholder: |
1. I did this...
2. And then that...
3. Finally, I clicked here.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
placeholder: What you expected to happen.
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
placeholder: What happened instead.
validations:
required: true
- type: textarea
id: debug-info
attributes:
label: Debug info
placeholder: |
- Logs of your full build command, with the `--verbose` flag. Put them in a ```code block``` !
- If the bug happens at app run time, the in-app DevTools console logs (open it with F12)
- Error messages, screenshots, screencasts, anything relevant!
validations:
required: false
- type: input
id: nativefier-version
attributes:
label: Nativefier version
placeholder: "nativefier --version"
validations:
required: true
- type: input
id: node-version
attributes:
label: Node.js version
placeholder: "node --version"
validations:
required: true
- type: input
id: npm-version
attributes:
label: npm version
placeholder: "npm --version"
validations:
required: true
- type: input
id: os
attributes:
label: OS
placeholder: "For example: Windows 10 build 1809"
validations:
required: true

View File

@ -1,64 +0,0 @@
---
name: Bug report
about: Report something broken
labels: bug
---
<!-- Help us help you, and take the time to fill this template 🙂.
An incomprehensible bug report is a useless bug report.
=========================================================
Incomprehensible / incomplete bug reports will be closed.
=========================================================
-->
**Homework**
- [ ] I looked at `nativefier --help` and https://github.com/jiahaog/nativefier/blob/master/docs/api.md
- [ ] I searched existing issues, open & closed. Yes, my bug is new.
- [ ] I'm using the latest version available at https://github.com/jiahaog/nativefier/releases
**Bug description**
A clear and concise description of what the bug is.
**Steps to reproduce**
Give your ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:
```
nativefier --verbose --some-option https://mysite.com
<paste your verbose build logs too>
```
**Expected behavior**
What you expected to happen.
**Actual behavior**
What happened instead.
**Debug info**
- Console logs of your `nativefier` build command, with `--verbose` flag
- If the bug happens at app run time, the in-app DevTools console logs (open it with F12)
- Error messages
- Screenshots
- Anything else relevant!
**Context**
- Nativefier: (for example: 9.1.0)
- Node.js: (for example: 14.6.0)
- Npm: (for example: 6.14.7)
- OS: (for example: Windows 10 build 1809)
- Is it a regression? If yes, what's the last working / first broken version?
- Additional context: (for example: "I'm behind a proxy, with configuration X and protocol Y")

45
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Feature request
description: Suggest an idea for Nativefier
labels: ["feature-request"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request 🙂! Help us help you, **fill this form thoroughly**. An incomplete feature request is a useless feature request.
- type: checkboxes
id: homework
attributes:
label: Homework
options:
- label: I took the time to write a good, descriptive issue title
required: true
- label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md), no existing option fits my needs.
required: true
- label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds.
required: true
- label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my feature request is new.
required: true
- label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases). Yes, the feature I'm requesting isn't in it.
required: true
validations:
required: true
- type: textarea
id: problem-statement
attributes:
label: Problem statement
description: A clear and concise description of what your feature would be.
placeholder: |
For example:
Nativefier should XYZ, ... details details details...
Existing option --something is not what I want, because ...
validations:
required: true
- type: textarea
id: motivation-and-context
attributes:
label: Motivation & context
placeholder: |
What makes you want this feature?
Where does it come from?
validations:
required: true

View File

@ -1,50 +0,0 @@
---
name: Feature request
about: Suggest an idea for Nativefier
labels: feature-request
---
<!-- Help us help you, and take the time to fill this template 🙂.
An incomprehensible feature request is a useless feature request.
==============================================================
Incomprehensible / incomplete feature requests will be closed.
==============================================================
-->
**Homework**
- [ ] I looked at `nativefier --help` and https://github.com/jiahaog/nativefier/blob/master/docs/api.md , no existing option fits my needs.
- [ ] I searched existing issues, open & closed. Yes, my feature request is new.
- [ ] I'm using the latest version available at https://github.com/jiahaog/nativefier/releases
**Problem statement**
A clear and concise description of what the problem is. For example: *Nativefier should [...]. I need it because [...]. Existing options [...] are not exactly what I want, because [...]*
If related to a Nativefier config, provide your ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:
```
nativefier --verbose --some-option https://mysite.com
<paste your verbose build logs, if relevant to your feature request>
```
**Suggested solution**
A clear and concise description of what you want to happen.
**Alternative solutions**
A clear and concise description of workarounds you've considered/tried.
**Context**
- Nativefier: (for example: 9.1.0)
- Node.js: (for example: 14.6.0)
- Npm: (for example: 6.14.7)
- OS: (for example: Windows 10 build 1809)
- Additional context: (for example: "I'm behind a proxy, with configuration X and protocol Y")

View File

@ -1,54 +0,0 @@
---
name: Question
about: Ask for help
labels: question
---
<!-- Help us help you, and take the time to fill this template 🙂.
An incomprehensible question is a useless question.
=======================================================
Incomprehensible / incomplete questions will be closed.
=======================================================
-->
**Homework**
- [ ] I looked at `nativefier --help` and https://github.com/jiahaog/nativefier/blob/master/docs/api.md
- [ ] I searched existing issues, open & closed. Yes, my question is new.
- [ ] I'm using the latest version available at https://github.com/jiahaog/nativefier/releases
**Your question**
Your question, expressed clearly and concisely.
**Steps to reproduce**
If you already have a Nativefier command you're struggling with, paste ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:
```
nativefier --verbose --some-option https://mysite.com
<paste your verbose build logs, if relevant to your question>
```
**Debug info**
If applicable,
- Console logs of your attempted `nativefier` build command, with `--verbose` flag
- Error messages
- Screenshots
- Anything else relevant!
**Context**
- Nativefier: (for example: 9.1.0)
- Node.js: (for example: 14.6.0)
- Npm: (for example: 6.14.7)
- OS: (for example: Windows 10 build 1809)
- Additional context: (for example: "I'm behind a proxy, with configuration X and protocol Y")

78
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@ -0,0 +1,78 @@
name: Question
description: Ask for help
labels: ["question"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report 🙂! Help us help you, **fill this form thoroughly**. A cryptic question is a question unlikely to be answered.
- type: checkboxes
id: homework
attributes:
label: Homework
options:
- label: I took the time to write a good, descriptive issue title
required: true
- label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md).
required: true
- label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds.
required: true
- label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my question is new.
required: true
- label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases).
required: true
validations:
required: false
- type: textarea
id: question
attributes:
label: Your question
description: Your question, expressed clearly and concisely.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: "If you already have a Nativefier command you're struggling with, paste ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:"
value: |
```
nativefier --verbose --some-option https://mysite.com
<paste your verbose build logs, if relevant to your question>
```
validations:
required: false
- type: textarea
id: debug-info
attributes:
label: Debug info
placeholder: |
Error messages, screenshots, screencasts, anything relevant!
- type: input
id: nativefier-version
attributes:
label: Nativefier version
placeholder: "nativefier --version"
validations:
required: true
- type: input
id: node-version
attributes:
label: Node.js version
placeholder: "node --version"
validations:
required: true
- type: input
id: npm-version
attributes:
label: npm version
placeholder: "npm --version"
validations:
required: true
- type: input
id: os
attributes:
label: OS
placeholder: "For example: Windows 10 build 1809"
validations:
required: true

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

79
.github/generate-changelog vendored Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Updates the changelog and version in the package.json
# Will also create a commit with these changes locally
#
# Usage:
# ./.github/generate-changelog -- "7.0.0"
#
# Prerequisites:
# - On master branch
# - No uncommitted changes
#
# Dependencies:
# - git-extras: https://github.com/tj/git-extras/blob/master/Installation.md
# - jq: https://stedolan.github.io/jq/download/
set -eo pipefail
echo 'HEY YOU. Before you release, here is a report of outdated dependencies.'
echo ' - Red upgrades fulfill semver and do *not* need any action'
echo ' - Yellow upgrades *do* need looking at changelogs for breaking changes, and updating package.json'
echo
echo 'CLI:'
npm out || true
echo
echo 'App:'
cd app; npm out || true; cd ..
echo
echo 'Okay with this, or care to do/plan a few upgrades?'
echo 'Press any key to continue, or Ctrl+C to abort'
read -r
echo 'HEY YOU, again. Did you run the quick pre-release smoke test? ( npm run test:manual )'
echo 'Press any key to continue, or Ctrl+C to abort'
read -r
# Checks if we are on the master branch
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$BRANCH" != 'master' ]]; then
echo 'ERROR: not on master branch' >&2
exit 1;
fi
# Checks if there are uncommitted changes
git diff-index --quiet HEAD -- || (echo 'ERROR: there are uncommitted changes' >&2 && exit 1)
VERSION="$1"
# Validates the $VERSION
SEMVER_REGEX='^([0-9]+\.){2}([0-9]+)$'
if ! [[ $VERSION =~ $SEMVER_REGEX ]]; then
echo "ERROR: Version '$VERSION' is invalid " >&2
exit 1
fi
# 1. Update the version in the package.json
cat package.json | jq ".version = \"$VERSION\"" > package.json.tmp
mv package.json.tmp package.json # workaround for in-place jq editing
# 2. Compile new commits from CHANGELOG.md, and open it in your EDITOR for cleanup
git changelog CHANGELOG.md --tag "$VERSION"
# 3. Commit the changes
git add CHANGELOG.md
git add package.json
git commit -m "Update changelog for \`v$VERSION\`"
# 4. Create an annotated tag
git tag -a "v$VERSION" -m "v$VERSION"
# 5. List remaining work
echo
echo 'Please verify commit & tag look fine in Git, then:'
echo ' 1. Push: git push --follow-tags origin master'
echo ' 2. Create a GitHub Release at https://github.com/nativefier/nativefier/releases ,'
echo " using created tag v$VERSION and with title \"Nativefier v$VERSION\" (yes, with a \"v\")."
echo
echo 'GitHub Action "publish" will react on the new release, and publish it to npm.'
echo 'The new version will be visible on npm within a few minutes/hours.'

125
.github/manual-test vendored Executable file
View File

@ -0,0 +1,125 @@
#!/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
function launch_app() {
printf '\n*** Running app\n'
if [ "$(uname -s)" = "Darwin" ]; then
open -a "$1/$2-darwin-x64/$2.app"
elif [ "$(uname -o)" = "Msys" ]; then
"$1/$2-win32-x64/$2.exe"
else
"$1/$2-linux-x64/$2"
fi
}
function do_cleanup() {
if [ -n "$1" ]; then
printf '\n***** Deleting test dir %s *****\n' "$1"
rm -rf "$1"
printf '\n'
fi
}
function request_feedback() {
printf '\nDid everything work as expected? [yN] '
read -r response
do_cleanup "$1"
if [ "$response" != 'y' ]; then
echo "Back to fixing"
exit 1
fi
echo "Yayyyyyyyyyyy"
}
printf "\n***** SMOKE TEST 1: Setting up test and building app... *****\n"
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
nativefier_dir="$script_dir/.."
pushd "$nativefier_dir"
tmp_dir=$(mktemp -d -t nativefier-manual-test-XXXXX)
name="nativefier-smoke-test-1"
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"
node ./lib/cli.js 'https://npmjs.com/' \
--inject "$injected_css" \
--inject "$injected_js" \
--name "$name" \
"$tmp_dir"
printf '\n***** SMOKE TEST 1: Test checklist *****
- Context menu -> Open Link In New Window works
- MAC ONLY: Context menu -> Open Link In New Tab works
- Keyboard shortcuts: {back, forward, zoom in/out/zero} work
- Console: no Electron runtime deprecation warnings/error logged'
launch_app "$tmp_dir" "$name"
request_feedback "$tmp_dir"
# ------------------------------------------------------------------------------
printf '\n***** SMOKE TEST 2: Setting up test and building app... *****\n'
tmp_dir=$(mktemp -d -t nativefier-manual-test-tray-XXXXX)
name='nativefier-smoke-test-2'
node ./lib/cli.js 'https://google.com/' \
--name "$name" \
--tray \
"$tmp_dir"
printf '\n***** SMOKE TEST 2: Test checklist *****
- Should have an app with a tray icon
- Console: no Electron runtime deprecation warnings/error logged'
launch_app "$tmp_dir" "$name"
request_feedback "$tmp_dir"
# ------------------------------------------------------------------------------
printf '\n***** SMOKE TEST 3: Setting up test and building app... *****\n'
tmp_dir=$(mktemp -d -t nativefier-manual-test-start-in-tray-XXXXX)
name='nativefier-smoke-test-3'
node ./lib/cli.js 'https://google.com/' \
--name "$name" \
--tray start-in-tray \
"$tmp_dir"
printf '\n***** SMOKE TEST 3: Test checklist *****
- Should have an app that does not show a window initially,
but will have a tray icon that will show the window.
- Console: no Electron runtime deprecation warnings/error logged'
launch_app "$tmp_dir" "$name"
request_feedback "$tmp_dir"
# ------------------------------------------------------------------------------
printf '\n***** SMOKE TEST 4: Setting up test and building app... *****\n'
tmp_dir=$(mktemp -d -t nativefier-manual-test-get-media-devices)
name='nativefier-smoke-test-4'
node ./lib/cli.js 'https://meet.jit.si/nativefier-test' \
--name "$name" \
"$tmp_dir"
printf '\n***** SMOKE TEST 4: Test checklist *****
- Join the Jitsi meeting and try to share your screen
(third button from the left in the bottom bar)
- An overlay should appear where you can select a screen/window to share
This presently does not work in MacOS as you would have to give the app
"Screen Recording" permissions, but you can''t for an app in the temp directory.
- After selecting a screen, a thumbnail of the shared screen should appear on
the top right
- Console: no Electron runtime deprecation warnings/error logged'
launch_app "$tmp_dir" "$name"
request_feedback "$tmp_dir"

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -8,26 +8,74 @@ on:
branches: branches:
- master - master
# - Bumping the *minimum* required Node version? You must bump:
# 1. package.json -> engines.node
# 2. package.json -> devDependencies.@types/node
# 3. tsconfig.json -> {target, lib}
# 4. .github/workflows/ci.yml -> node-version
# - Bumping the *maximum* tested Node version? You must bump also: publish.yml
jobs: jobs:
build: lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 20
uses: actions/setup-node@v2
with:
node-version: 20
cache: 'npm'
cache-dependency-path: |
npm-shrinkwrap.json
app/npm-shrinkwrap.json
package-lock.json
app/package-lock.json
- env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run lint
playwright:
runs-on: windows-latest # Doesn't work on headless ubuntu, and is slow on mac
steps:
- uses: actions/checkout@v2
- name: Use Node.js 20
uses: actions/setup-node@v2
with:
node-version: 20
cache: 'npm'
cache-dependency-path: |
npm-shrinkwrap.json
app/npm-shrinkwrap.json
package-lock.json
app/package-lock.json
- env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run test:playwright
timeout-minutes: 5
# Useful to debug PlayWright tests failing in CI
# env:
# DEBUG: pw:browser*
tests:
strategy: strategy:
matrix: matrix:
node-version: node-version:
- 15.x - '20'
- 14.x # Changing this? Remind to keep linter conditions below and in publish.yml aligned. - '16' # the oldest we require in package.json -> engines.node, to check we run on this minimum
- 12.x
- 10.x
platform: [ubuntu-latest, macos-latest, windows-latest] platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm run dev-up cache: 'npm'
- run: npm run build cache-dependency-path: |
# Only run linter once, for faster CI. Align the verisons of Node here with above and publish.yml. npm-shrinkwrap.json
- if: matrix.platform == 'ubuntu-latest' && matrix.node-version == '14.x' app/npm-shrinkwrap.json
run: npm run lint package-lock.json
- run: npm test app/package-lock.json
- env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run test:noplaywright

View File

@ -4,19 +4,48 @@ on:
types: types:
- created - created
jobs: jobs:
playwright:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2 # Setup .npmrc file to publish to npm
with:
node-version: '20' # Align the version of Node here with ci.yml.
registry-url: 'https://registry.npmjs.org'
- run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run test:playwright
timeout-minutes: 5
build: build:
needs: playwright
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
# Setup .npmrc file to publish to npm - uses: actions/setup-node@v2 # Setup .npmrc file to publish to npm
- uses: actions/setup-node@v1
with: with:
# Align the version of Node here with ci.yml. node-version: '20' # Align the version of Node here with ci.yml.
node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: npm run dev-up - run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run build - run: npm run test:noplaywright
- run: npm test - run: npm run lint
- run: npm publish - run: npm publish
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
docker:
needs: [ playwright, build ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build . --file Dockerfile --tag "nativefier/nativefier:latest"
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Tag and push Docker image
run: |
docker tag "nativefier/nativefier:latest" "nativefier/nativefier:${GITHUB_REF_NAME}"
docker push "nativefier/nativefier:latest"
docker push "nativefier/nativefier:${GITHUB_REF_NAME}"

19
.gitignore vendored
View File

@ -1,9 +1,6 @@
# OSX # OSX
.DS_Store .DS_Store
# Node.js
package-lock.json
# ignore compiled lib files # ignore compiled lib files
lib* lib*
app/lib/* app/lib/*
@ -11,9 +8,13 @@ app/dist/*
built-tests built-tests
# commit a placeholder to keep the app/lib directory # commit a placeholder to keep the app/lib directory
app/inject
!app/inject/_placeholder
!app/lib/.placeholder !app/lib/.placeholder
dist dist
package-lock.json
app/package-lock.json
# Logs # Logs
logs logs
@ -44,11 +45,23 @@ build/Release
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules node_modules
# Python virtual environment in case it's created for the Castlabs code signing tool
venv
# IntelliJ project files # IntelliJ project files
.idea .idea
*.iml *.iml
.run
out out
gen gen
# Builds when testing npm pack # Builds when testing npm pack
nativefier*.tgz nativefier*.tgz
.vscode
# https://github.com/nektos/act
.actrc
tsconfig.tsbuildinfo
scripts

View File

@ -1,12 +1,12 @@
/* /*
!lib/ !lib/
!icon-scripts !icon-scripts
!npm-shrinkwrap.json
.DS_Store .DS_Store
src/ src/
*eslintrc.js *eslintrc.js
*eslintrc.yml *eslintrc.yml
*tsconfig.tsbuildinfo *tsconfig.tsbuildinfo
*package-lock.json
*tsconfig.json *tsconfig.json
*jestSetupFiles* *jestSetupFiles*
*-test.js *-test.js
@ -19,3 +19,5 @@ app/*
!app/inject/ !app/inject/
!app/nativefier.json !app/nativefier.json
!app/package.json !app/package.json
!app/npm-shrinkwrap.json
.vscode/

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
16

View File

@ -1,3 +0,0 @@
arrowParens: always
singleQuote: true
trailingComma: all

1303
API.md Normal file

File diff suppressed because it is too large Load Diff

300
CATALOG.md Normal file
View File

@ -0,0 +1,300 @@
# Build Commands Catalog
Below you'll find a list of build commands contributed by the Nativefier community. They are here as examples, to help you nativefy "complicated" apps that need a bit of elbow grease to work. We need your help to enrich it, as long as you follow these two guidelines:
1. Only add sites that require something special! No need to document here that `simplesite.com` works with a simple `nativefier simplesite.com` 🙂.
2. Please add commands with the _strict necessary_ to make an app work. For example,
- Yes to mention that `--widevine` or some `--browserwindow-options` are necessary...
- ... but don't add other flags that are pure personal preference (e.g. `--disable-dev-tools` or `--disk-cache-size`).
---
## General recipes
### Videos dont play
Some sites like [HBO Max](https://github.com/nativefier/nativefier/issues/1153) and [Udemy](https://github.com/nativefier/nativefier/issues/1147) host videos using [DRM](https://en.wikipedia.org/wiki/Digital_rights_management).
For those, try passing the [`--widevine`](API.md#widevine) option.
### Settings cached between app rebuilds
You might be surprised to see settings persist after rebuilding your app.
This occurs because the app cache lives separately from the app.
Try deleting your app's cache, found at `<your_app_name_lower_case>-nativefier-<random_id>` in your OSs "App Data" directory (Linux: `$XDG_CONFIG_HOME` or `~/.config` , MacOS: `~/Library/Application Support/` , Windows: `%APPDATA%` or `C:\Users\yourprofile\AppData\Roaming`)
### Window size and position
This allows the last set window size and position to be remembered and applied
after your app is restarted. Note: PR welcome for a built-in fix for that :) .
```sh
nativefier 'https://open.google.com/'
--inject window.js
```
Note: [Inject](https://github.com/nativefier/nativefier/blob/master/API.md#inject)
the following javascript as `windows.js` to prevent the window size and position to reset.
```javascript
function storeWindowPos() {
window.localStorage.setItem('windowX', window.screenX);
window.localStorage.setItem('windowY', window.screenY);
}
window.moveTo(window.localStorage.getItem('windowX'), window.localStorage.getItem('windowY'));
setInterval(storeWindowPos, 250);
```
---
## Site-specific recipes
### Google apps
Lying about the User Agent is required, else Google Login will notice your
"Chrome" isn't a real Chrome, and will: 1. Refuse login, 2. Break notifications.
This example documents Google Sheets, but is applicable to other Google apps,
e.g. Google Calendar, GMail, etc. If `firefox` doesnt work, try `safari` .
```sh
nativefier 'https://docs.google.com/spreadsheets' \
--user-agent firefox
```
### Outlook
```sh
nativefier 'https://outlook.office.com/mail'
--internal-urls '.*?(outlook.live.com|outlook.office365.com).*?'
--file-download-options '{"saveAs": true}'
--browserwindow-options '{"webPreferences": { "webviewTag": true, "nodeIntegration": true, "nodeIntegrationInSubFrames": true } }'
```
Note: `--browserwindow-options` is needed to allow pop-outs when creating/editing an email.
### Udemy
```sh
nativefier 'https://www.udemy.com/'
--internal-urls '.*?udemy.*?'
--file-download-options '{"saveAs": true}'
--widevine
```
Note: most videos will work, but to play some DRMed videos you must pass `--widevine` AND [sign the app](https://github.com/nativefier/nativefier/issues/1147#issuecomment-828750362).
### HBO Max
```sh
nativefier 'https://play.hbomax.com/'
--widevine
--enable-es3-apis
&& python -m castlabs_evs.vmp sign-pkg 'name_of_the_generated_hbo_app'
```
Note: as for Udemy, `--widevine` + [app signing](https://github.com/nativefier/nativefier/issues/1147#issuecomment-828750362) is necessary.
### WhatsApp
```sh
nativefier 'https://web.whatsapp.com/'
--inject whatsapp.js
```
With this `--inject` in `whatsapp.js` (and maybe more, see [#1112](https://github.com/nativefier/nativefier/issues/1112)):
```javascript
if ('serviceWorker' in navigator) {
caches.keys().then(function (cacheNames) {
cacheNames.forEach(function (cacheName) {
caches.delete(cacheName);
});
});
}
```
Another option to see WhatsApp or WhatsApp Business more macOS-like (macos only):
```sh
nativefier https://web.whatsapp.com --name 'WhatsApp Business' --counter true --darwin-dark-mode-support true --title-bar-style hidden --inject whatsappmacos.css
```
with this `whatsappmacos.css` to make the window draggable, and move the user avatar to the right:
```css
header > div:first-child {
flex: 0 0 auto;
margin-right: 15px;
}
div#app > div.os-mac > span:first-child {
position: fixed;
top: 0;
z-index: 1000;
width: 100%;
height: 59px;
pointer-events: none;
-webkit-app-region: drag;
}
```
### Spotify
```sh
nativefier 'https://open.spotify.com/'
--widevine
--inject spotify.js
--inject spotify.css
```
Notes:
- You might have to pass `--user-agent firefox` to circumvent Spotify's detection that your browser isn't a real Chrome. But [maybe not](https://github.com/nativefier/nativefier/issues/1195#issuecomment-855003776).
- [Inject](https://github.com/nativefier/nativefier/blob/master/API.md#inject) the following javascript as `spotify.js` to prevent "Unsupported Browser" messages.
```javascript
function dontShowBrowserNoticePage() {
const browserNotice = document.getElementById('browser-support-notice');
console.log({ browserNotice });
if (browserNotice) {
// When Spotify displays the browser notice, it's not just the notice,
// but the entire page is focused on not allowing you to proceed.
// So in this case, we hide the body element (so nothing shows)
// until our JS deletes the service worker and reload (which will actually load the player)
document.getElementsByTagName('body')[0].style.display = 'none';
}
}
function reload() {
window.location.href = window.location.href;
}
function nukeWorkers() {
dontShowBrowserNoticePage();
if ('serviceWorker' in navigator) {
caches.keys().then(function (cacheNames) {
cacheNames.forEach(function (cacheName) {
console.debug('Deleting cache', cacheName);
caches.delete(cacheName);
});
});
navigator.serviceWorker.getRegistrations().then((registrations) => {
registrations.forEach((worker) =>
worker
.unregister()
.then((u) => {
console.debug('Unregistered worker', worker);
reload();
})
.catch((e) =>
console.error('Unable to unregister worker', error, { worker }),
),
);
});
}
}
document.addEventListener('DOMContentLoaded', () => {
nukeWorkers();
});
if (document.readyState === 'interactive') {
nukeWorkers();
}
```
- It is also required to [sign the app](https://github.com/nativefier/nativefier/blob/master/API.md#widevine), or many songs will not play.
- To hide all download links (as if you were in the actual app), [inject](https://github.com/nativefier/nativefier/blob/master/API.md#inject) the following CSS as `spotify.css`:
```css
a[href='/download'] {
display: none;
}
```
### Notion
You can use Notion pages with Nativefier without much hassle, but Notion itself does not present an easy way to use HTML buttons. As such, if you want to use Notion Pages as a quick way to make dashboards and interactive panels, you will be restricted to only plain links and standard components.
With Nativefier you can now extend Notion's functionality and possibilities by adding HTML buttons that can call other javascript functions, since it enables you to inject custom Javascript and CSS.
```sh
nativefier 'YOUR_NOTION_PAGE_SHARE_URL'
--inject notion.js
--inject notion.css
```
Notes:
- You can inject the notion.js and notion.css files by copying them to the resources/app/inject folder of your nativefier app.
- In your Notion page, use [notionbutton]BUTTON_TEXT|BUTTON_ACTION[/notionbutton], where BUTTON_TEXT is the text contained in your button and BUTTON_ACTION is the action which will be called in your JS function.
```javascript
/* notion.js */
// First, we replace all placeholders in our Notion page to add our interactive buttons to it.
window.onload =
setTimeout(function(){
let htmlCode = document.body.getElementsByTagName("*");
for (let i = 0; i <= htmlCode.length; i++) {
if(htmlCode[i] && htmlCode[i].innerHTML){
let match = htmlCode[i].innerHTML.match(/\[notionbutton\]([\s\S]*?)\[\/notionbutton\]/);
if (match && typeof match == 'object'){
let btnarray = match['1'].split("|");
let btn_text = btnarray[0];
let btn_action = btnarray[1];
htmlCode[i].innerHTML = htmlCode[i].innerHTML.replace(match['0'], "<button class=\"btn-notion\" btnaction=\"" + btn_action + "\" >"+btn_text+"</button>");
}
}
}
let buttons = document.querySelectorAll(".btn-notion");
for (let j=0; j <= buttons.length; j++){
if(buttons[j].hasAttribute("btnaction")){
buttons[j].onclick = function () { runAction(buttons[j].getAttribute("btnaction")) };
}
}
}, 3000);
// And then we define your action below, according to our needs
function runAction(action) {
switch(action){
case '1':
alert('Nice One!');
break;
default:
alert('Hello World!');
}
}
```
After that, set your css file as follows:
```css
.notion-topbar{ /* hiding notion's default navigation bar for a more "app" feeling */
display:none;
}
.btn-notion{ /* defining some style for our buttons */
background-color:#FFC300;
color: #333333;
}
.notion-selectable.notion-page-block.notion-collection-item span{
pointer-events: auto !important; /* notion prevents clicks on items inside databases. Use this to remove that. */
}
```
### Microsoft Teams
You can get an almost macOS look-alike using this:
```sh
nativefier https://teams.microsoft.com --name 'Microsoft Teams' --counter true --darwin-dark-mode-support true --title-bar-style hidden --internal-urls "(.*)" --inject teamsapp.css
```
Note that the `--internal-urls` argument is necessary to login.
Inject the following `teamsapp.css` file to hide the download button at the bottom left and the Office 365 apps waffle button at the top left:
```css
get-app-button.ts-sym.app-bar-link {
display: none;
}
button#ts-waffle-button {
display: none;
}
```

View File

@ -1,4 +1,580 @@
52.0.0 / 2023-08-25
===================
**[BREAKING]**
* Update to Electron 25.7 (#1566)
* Update to Electron 25 (#1559)
51.0.1 / 2023-08-04
===================
* `npm i` in the Dockerfile to esnure we have what we need to build + test (#1557)
51.0.0 / 2023-08-03
===================
**[BREAKING]**
* Update Electron to 21 + Node to 16 (#1550)
* Update link to Development Guide (#1544)
50.1.1 / 2023-03-27
===================
* Fix shrinkwrap versions back to lockfileVersion 1 (node 12)
* Fix typo "electon" -> "electron" (#1492)
50.1.0 / 2023-03-24
===================
* Update outdated shrinkwrap files
* Add getDisplayMedia and PipeWire support (#1477)
50.0.1 / 2022-11-07
===================
* Windows: Fix "Maximize window visual glitch" (fix #1447) (PR #1448)
* External URL protocols: add zoommtg as no-confirmation (PR #1463)
* CATALOG.md: MS Teams CSS inject (PR #1469), WhatsApp native macOS look CSS (PR #1468)
* Bump default Electron from 19.0.17 to 19.1.4, with security fixe
* CI: test on 12 and **19**, now that 19 is out
* Upgrade CLI & App dependencies
50.0.0 / 2022-09-17
===================
**[BREAKING]** Add validation to opening external URLs in desktop handler (fix #1459)
This will, for security, refuse loading of certain external of two kinds.
One: using dubious URL schemes, two: including nasty characters.
Blocking URLs will be accompanied by a window explaining what's going on,
and linking to a discussion thread where you can report false positives.
Hopefully not _BREAKING_ much (the behavior should now be aligned with
what browsers do), but web weirdness happens. Shout and we'll tweak.
Also,
* Fix double-navigation to pages (fix #1452)
* Upgrade cli+app dependencies
* Bump default Electron to 19.0.17 (from .14), with security fixes
- https://github.com/electron/electron/releases/tag/v19.0.15
- https://github.com/electron/electron/releases/tag/v19.0.16
- https://github.com/electron/electron/releases/tag/v19.0.17
49.0.1 / 2022-08-28
===================
* Bump default Electron to 19.0.14 (from .10), with security fixes
- https://github.com/electron/electron/releases/tag/v19.0.11
- https://github.com/electron/electron/releases/tag/v19.0.12
- https://github.com/electron/electron/releases/tag/v19.0.13
- https://github.com/electron/electron/releases/tag/v19.0.14
* macOS: Move handling of "Universal" apps to electron-packager instead of our own thing (PR #1443)
* Upgrade cli+app dependencies
49.0.0 / 2022-07-30
===================
**[BREAKING]** 49.0.0 doesn't have more breaking changes than 48.0.0, but I'm
releasing a new major release anyway to signal one particularly noteworthy
breaking change in Electron 19 that I failed to pass along to you in 48.0.0:
**The `ia32` arch (a.k.a. `i386` or `x86/32bit`) is no longer supported.**
People still running Nativefier apps on old ia32 machines, feel free to keep
passing a flag `--electron-version 18.x.y` *while it works*. Note however that
we won't be testing it, and future Nativefier versions may depend on upcoming
Electron APIs that will crash your electron18-app-packaged-by-future-Nativefier.
The deprecation is an upstream Electron decision, and there's nothing we will
do about it. Thx @TheCleric for the catch.
Also,
* macOS: Fix "main window cannot be activated" (fix #1415, PR #1417)
* Bump default Electron from 19.0.9 to [19.0.10](https://github.com/electron/electron/releases/tag/v19.0.10)
* Fix loud axios "fetch" warning (https://github.com/nativefier/gitcloud-client/pull/4)
* Fix playwright tests on Linux (#1440)
* Docker: upgraded base node-alpine image from 12 to LTS (currently 16)
48.0.0 / 2022-07-24
===================
* **[BREAKING]** Bump default Electron to 19.0.9 (from 18.3.5)
As usual, we did our best to adapt to Electron breaking changes, but
patches welcome to fix regressions. If unable to submit a patch,
feel free to revert to Nativefier 47.2.1, or pass `-e 18.3.5` for a
_temporary_ downgrade (it will work for a while, but not forever).
Official release notes: https://www.electronjs.org/blog/electron-19-0
Detailed release notes:
- https://github.com/electron/electron/releases/tag/v19.0.0
- https://github.com/electron/electron/releases/tag/v19.0.1
- https://github.com/electron/electron/releases/tag/v19.0.2
- https://github.com/electron/electron/releases/tag/v19.0.3
- https://github.com/electron/electron/releases/tag/v19.0.4
- https://github.com/electron/electron/releases/tag/v19.0.5
- https://github.com/electron/electron/releases/tag/v19.0.6
- https://github.com/electron/electron/releases/tag/v19.0.7
- https://github.com/electron/electron/releases/tag/v19.0.8
- https://github.com/electron/electron/releases/tag/v19.0.9
* CATALOG.md: add a new recipe for using interactive buttons on Notion (PR #1430)
* GitHub Issues: switch from "Issue templates" to new & better "Issue forms" (fix #1258) (PR #1425)
* Maintenance: upgrade Jest, fix PlayWright tests
47.2.1 / 2022-06-27
===================
* macOS: fix incorrect "Back" keyboard shortcut (fix #1426)
* Bump default Electron to 18.3.5 (from 18.3.1), with security fixes:
https://github.com/electron/electron/releases/tag/v18.3.2
https://github.com/electron/electron/releases/tag/v18.3.3
https://github.com/electron/electron/releases/tag/v18.3.4
https://github.com/electron/electron/releases/tag/v18.3.5
* Update dependencies
47.2.0 / 2022-05-30
===================
* Handle `open-url` event: support "deep-linking" e.g. for mailto links (PR #1418, fix #1412)
* Bump default Electron to 18.3.1 (from 18.2.0), with security fixes:
https://github.com/electron/electron/releases/tag/v18.2.1
https://github.com/electron/electron/releases/tag/v18.2.2
https://github.com/electron/electron/releases/tag/v18.2.3
https://github.com/electron/electron/releases/tag/v18.2.4
https://github.com/electron/electron/releases/tag/v18.3.0
https://github.com/electron/electron/releases/tag/v18.3.1
* Update dependencies
* Docs: {API, README, CATALOG}.md cleanups
47.1.3 / 2022-05-02
===================
* Auto-internal URLs: add VMWare Workspace ONE + SecurID (PR #1391, fix #1390)
* `--counter`: accept colon character; useful for time-tracking apps with hour:min in title (PR #1378)
* Windows: correctly set notifications name - not electron.app.YOURAPPNAME (PR #1394)
* macOS: support "universal" architecture (fix #1384 #1398, PR #1386)
* macOS: fix "Open In New Tab" (fix #1260, PR #1385)
* macOS: Change "Paste and Match Style" shortcut to match Apple's HIG guidelines (PR #1387, fix #404)
* macOS: Bump minimum macOS version from 10.9 to 10.10 (see #1404)
This has been effectively been the case since a long time, it was just misdocumented.
Thus, not really a breaking change, and not major-bumping.
* CATALOG.md: add a new "General recipes" section, with one to restore app position/size (PR #1349)
* CI: Add integration testing to the app, using Playwright (PR #1397)
* CI: Speed it up by parallelize tasks
* CI: Bump max tested version of Node for CI/Publish from 17 to 18
* Update dependencies
* Bump default Electron to 18.2.0 (from 18.0.3), with security fixes:
https://github.com/electron/electron/releases/tag/v18.0.4
https://github.com/electron/electron/releases/tag/v18.1.0
https://github.com/electron/electron/releases/tag/v18.2.0
47.0.0 / 2022-04-10
===================
* **[BREAKING]** Bump default Electron to 18.0.3 (from 16.2.2)
As usual, we did our best to adapt to Electron breaking changes in 17/18,
but patches welcome to fix regressions. If unable to submit a patch, then
feel free to revert to Nativefier 46.2.1 or simply pass `-e 16.2.2` .
Release notes with breaking changes:
- https://www.electronjs.org/blog/electron-17-0
- https://www.electronjs.org/blog/electron-18-0
Detailed release notes:
- https://github.com/electron/electron/releases/tag/v17.0.0
- https://github.com/electron/electron/releases/tag/v18.0.0
- https://github.com/electron/electron/releases/tag/v18.0.1
- https://github.com/electron/electron/releases/tag/v18.0.2
- https://github.com/electron/electron/releases/tag/v18.0.3
46.2.1 / 2022-04-10
===================
* Bump default Electron to 16.2.1 (from 16.1.0), with security fixes:
- https://github.com/electron/electron/releases/tag/v16.1.1
- https://github.com/electron/electron/releases/tag/v16.2.0
- https://github.com/electron/electron/releases/tag/v16.2.1
- https://github.com/electron/electron/releases/tag/v16.2.2
* Upgrade dependencies lockfiles
46.2.0 / 2022-03-20
===================
* Bugfix: Strip LRM and RLM in Linux names (fix #1361, PR #1365)
* Bugfix: Remove extra whitespace in UserAgent (fix #1357, PR #1367)
* Docs: Fix broken link in `API.md` for `conceal` flag (PR #1364)
* Bump default Electron to 16.1.0 (from 16.0.9), with security fixes:
- https://github.com/electron/electron/releases/tag/v16.1.0
- https://github.com/electron/electron/releases/tag/v16.0.10
* Upgrade dependencies lockfiles
46.1.1 / 2022-02-14
===================
* Feature: Add "copy as plain text" in edit menu (PR #1351 @abhi12299, fix #1144)
* Bump default Electron to 16.0.9 (from 16.0.8), with security fixes
- https://github.com/electron/electron/releases/tag/v16.0.9
* Upgrade dependencies
46.1.0 / 2022-02-06
===================
* Add flag `--strict-internal-urls` to disable domain and subpath matching (PR #1340 @hbridge)
* Add flag `--quiet` flag to suppress all log output (PR #1342 @Nickersoft)
* Fix flag `--file-download-options` (PR #1350 @abhi12299, #1275)
* Allow setting default app destination with env. var. `NATIVEFIER_APPS_DIR` (PR #1339 @mattruzzi, #1336)
* Bump default Electron to 16.0.8, from 16.0.6
- https://github.com/electron/electron/releases/tag/v16.0.7
- https://github.com/electron/electron/releases/tag/v16.0.8
* Upgrade dependencies
* Docs:
- CATALOG.md: Document GCal needs lying about useragent for working notifications (fix #1292)
- API.md: Fix broken "insecurity options" link (PR #1345 @ZacharyTalis)
- README.md: mention Snap & AUR repos
- HACKING.md: add triage guidelines
46.0.4 / 2022-01-06
===================
* CI: (Attempt to) push tag, not unreadable SHA
46.0.3 / 2022-01-06
===================
* CI: Push Docker image to our org, not my personal account
46.0.2 / 2022-01-06
===================
* CI: Fix Docker Hub image build & push (PR #1100, thx @snpranav)
46.0.1 / 2022-01-06
===================
* Fix `--widevine` broken since 46.0.0 (thx @loxK)
* Bump default Electron from 16.0.5 to 16.0.6
- https://github.com/electron/electron/releases/tag/v16.0.6
46.0.0 / 2022-01-02
===================
* **[BREAKING]** Upgrade Electron from 13.6.3 & Chrome 91 to 16.0.5 & Chrome 96 (PR #1288)
We did our best to adapt to [Electron breaking changes](https://www.electronjs.org/docs/latest/breaking-changes) in 14/15/16, but as usual,
patches welcome to address regressions. For detailed release notes, see
- https://github.com/electron/electron/releases/tag/v14.0.0
- https://github.com/electron/electron/releases/tag/v15.0.0
- https://github.com/electron/electron/releases/tag/v16.0.0
- https://github.com/electron/electron/releases/tag/v16.0.1
- https://github.com/electron/electron/releases/tag/v16.0.2
- https://github.com/electron/electron/releases/tag/v16.0.3
- https://github.com/electron/electron/releases/tag/v16.0.4
- https://github.com/electron/electron/releases/tag/v16.0.5
* Build/CI: use setup-node-v2 cache to speed up build
45.0.8 / 2021-12-06
===================
* Fix 45.0.7 broken because of missing "chalk" dep (fix #1324)
45.0.7 / 2021-12-06
===================
* Use userAgentFallback for user-agent injection (PR #1316)
* Fix `--upgrade` (PR #1286)
* Bump default Electron to 13.6.3 with fixes & security fixes
- 13.6.3: https://github.com/electron/electron/releases/tag/v13.6.3
* Maintenance: documentation, scripts, dependencies bumps
* Display "we need your help" message when running CLI:
```
Hi! Nativefier is minimally maintained these days, and needs more hands.
If you have the time & motivation, help with bugfixes and maintenance is VERY welcome.
Please go to https://github.com/nativefier/nativefier and help how you can. Thanks.
```
45.0.6 / 2021-11-22
===================
* Fix notifications (PR #1308)
* Fix icon conversion scripts broken on recent macOS (fix #1277)
* Bump default Electron to 13.6.2, with bug fixes & security fixes
- 13.6.2: https://github.com/electron/electron/releases/tag/v13.6.2
* Maintenance: bump CI Nodejs to 17, Relock dependencies
45.0.5 / 2021-11-01
===================
* Bump default Electron to 13.6.1, with bug fixes & security fixes
- 13.5.2: https://github.com/electron/electron/releases/tag/v13.5.2
- 13.6.0: https://github.com/electron/electron/releases/tag/v13.6.0
- 13.6.1: https://github.com/electron/electron/releases/tag/v13.6.1
* Maintenance: Fix auth manual tests (#1287), Bumps (axios, eslint), Doc & script nits
45.0.4 / 2021-09-24
===================
* Actually actually (TM) include lockfile in npm artifacts, duuuuuh
45.0.3 / 2021-09-24
===================
* Actually include package-lock.json in npm artifacts, duh
45.0.2 / 2021-09-24
===================
* Fix regressions in opening windows/tabs, update browser versions (PR #1284)
* Make macOS "bundle identifier" mention Nativefier (fix #866) (PR #1259)
* Maintenance: Work around yargs coerce issue (PR #1283)
45.0.1 / 2021-09-20
===================
* Auto-internal login pages: add `(id|auth).atlassian.com` (fix #1265)
* API.md: document need to use CSS `!important` keyword (fix #1264)
* Bump default Electron to 13.4.0 with security fixes
* Maintenance: deps bumps, fix build:watch script, re-introduce a lockfile
45.0.0 / 2021-07-19
===================
* **[BREAKING]** Bump default Electron to 13.1.7 with Chrome 91 (PR #1230)
See https://www.electronjs.org/blog/electron-13-0
and https://www.electronjs.org/blog/electron-13-0#breaking-changes
44.0.7 / 2021-07-10
===================
* Fix badge/counter icon not being removed correctly (#1251, PR #1252)
* App context menu: add "Save Image", "Copy Image", "Copy Image Address" (PR #1256)
* Bump default Electron from 12.0.12 to 12.0.14. Changelogs:
[Electron 12.0.13](https://github.com/electron/electron/releases/tag/v12.0.13)
[Electron 12.0.14](https://github.com/electron/electron/releases/tag/v12.0.14)
* Maintenance: {API.md, HACKING.md} documentation, improve `generate-changelog`
44.0.6 / 2021-06-26
===================
Like 44.0.4, this release only contains one behind-the-scenes TS change with no user-visible changes.
It is here to let us narrow down on potential regressions that may have crept in.
It isn't especially scary, though. Do test it, use it, and report regressions!
* App: Enable TypeScript `strict:true`, more typescript-eslint rules, shared TS project (#1231)
44.0.5 / 2021-06-25
===================
* Fix "Reset Zoom" menu item (#1241, PR #1243)
* Fix `--tray start-in-tray` (#1225, PR #1235)
* Fix external URLs opening in a new Nativefier tab (#1228, PR #1229)
* Bump default Electron from 12.0.11 to 12.0.12. See changelog:
[Electron 12.0.12](https://github.com/electron/electron/releases/tag/v12.0.12)
* Maintenance: documentation
44.0.4 / 2021-06-15
===================
This release only contains one behind-the-scenes TS change with no user-visible changes.
It is here to let us narrow down on potential regressions that may have crept in.
It isn't especially scary, though. Do test it, use it, and report regressions!
* Enable TypeScript `strict:true`, and more typescript-eslint rules (#1223)
44.0.3 / 2021-06-15
===================
* Make CSS injection less brutal (#1222, #1227)
* Maintenance: README nits, Fix gitcloud 0.2.3 import
44.0.2 / 2021-06-07
===================
* Fix HTTP basic auth broken since 44.0.1 (fix #1219) (#1220)
* Fix tabs opening twice since 44.0.0 (fix #1209) (#1221)
44.0.1 / 2021-06-07
===================
* macOS: fix crash on activating main window (fix #1212) (PR #1213)
* macOS: fix fullscreen not working + menu refactor (fix #1206) (PR #1210)
44.0.0 / 2021-06-04
===================
* **[BREAKING]** Nativefier now requires node>=12.9 and npm>=6.9 (#1192)
We do our best to support the oldest Node we can (what Debian stable ships),
but we are also constrained by what our _own_ Node dependencies require.
Now is the time for a bump.
* Feature: Provide easy-to-use user-agent shortcodes (e.g. `firefox`) (#1198)
* Feature: Organize CLI flags into groups (for better `--help` usability) (#1191)
* Fix broken window popups (fix #1197, PR #1203)
* Fix allowing non-ascii app names like 微信读书 (fix #1056, PR #1207)
* Fix considering `login.microsoftonline.com` as internal login page (#1205)
* Bump default Electron from 12.0.7 to 12.0.10 with a couple of fixes. See changelogs for:
[12.0.8](https://github.com/electron/electron/releases/tag/v12.0.8),
[12.0.9](https://github.com/electron/electron/releases/tag/v12.0.9),
[12.0.10](https://github.com/electron/electron/releases/tag/v12.0.10).
* Maintenance: docs, tests tooling, deps bumps
43.1.3 / 2021-05-15
===================
👋 dear users. Two announcements in this release:
**This release (43.1.3) is the last release supporting Node 10.x**
We do our best to support the oldest Node we can (what Debian stable ships),
but we are also constrained by what our _own_ Node dependencies require.
It's time for a bump; the next release will be 44.0.0 and will require Node 12.
Also, introducing **[CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md),**
**a list of build commands contributed by the Nativefier community**, to help you
nativefy "complicated" apps that need a bit of elbow grease to work.
When stuck nativefying a specific site, go take a look, it might give you ideas :) .
* Fix crash on tab close (only try to inject CSS for valid web requests) (#939, PR #1181)
* Fix considering `shop.foo.com` and `blog.foo.com` as internal (PR #1171)
* CATALOG.md: build command library (fix #1166) (PR #1178)
* Bump to Electron 12.0.7
43.1.2 / 2021-05-03
===================
* Fix logging out users on upgrade / app recreate with same URL (fix #1176) (PR #1179)
43.1.1 / 2021-05-02
===================
* Fix crash in `preload.js` due to 3rd-party 'loglevel' (fix #1175, fix #1176) (PR #1177)
43.1.0 / 2021-05-01
===================
This is a chunky release! Warm thanks to all the contributors that helped shape it,
with a special shoutout to @TheCleric for a mountain of awesome work.
Features! (nothing breaking)
* Add a [`--upgrade`](https://github.com/nativefier/nativefier/blob/master/API.md#upgrade) option to easily upgrade an existing app (fix #1131) (PR #1138)
* Support defining a custom [`--bookmarks-menu`](https://github.com/nativefier/nativefier/blob/master/API.md#bookmarks-menu) (fix #1065) (PR #1155)
* Support setting apps [`--lang`](https://github.com/nativefier/nativefier/blob/master/API.md#lang)uage (fix #175) (PR #1173)
* Support creating self-contained "[`--portable`](https://github.com/nativefier/nativefier/blob/master/API.md#portable)" apps writing their app data to the app folder (fix #376) (PR #1168)
* Support opening URLs passed as arg to Nativefied apps (fix #405) (PR #1154)
Bugfixes!
* App: fix child windows not inheriting mainWindow properties (including userAgent), breaking some Google login pages (#1174)
* Fix `--inject`ing multiple css/js files (fix #458) (#1162)
* Fix `--widevine` by properly listening to `widevine-...` events (fix #1153) (PR #1164)
* Prompt to confirm when page is attempting to prevent unload (#1163)
* macOS: Fix crash when using `--tray` (fix #527) (PR #1156)
* macOS: Fix invisible icon (fix #942, fix #668) (PR #1156)
* Auto-internal login pages: add a missing Google login page (#1167)
Maintenance!
* Bump to [Electron 12.0.6](https://github.com/electron/electron/releases/tag/v12.0.6) with Chrome 89.0.4389.128 and security fixes
* Docs: add troubleshooting section for common issues (#1169), document signing `--widevine` apps like HBO Max & Udemy (#1147), misc fixes
* App: replace console.xyz calls with loglevel.xyz, with a level controlled by app argv `--verbose` (#1172)
* Auto-internal login pages: add test to ensure we don't regress on cases of SLDs
* CI: run in node 16, drop node 15. Run less node versions for faster CI; oldest supported / latest is enough
43.0.2 / 2021-04-13
===================
* Bump default Electron to 12.0.4 with Chrome 89.0.4389.114
This includes the recent security fixes.
See https://github.com/electron/electron/releases/tag/v12.0.3
and https://github.com/electron/electron/releases/tag/v12.0.4
43.0.1 / 2021-04-11
===================
* Add a `session-interaction` event to allow injected js to interact
with apps Electron `session` object (PR #1132)
* Automatically-internal login pages: add Apple ID (PR #1146), GitHub 2FA (PR #1140)
* Bump default Electron from 12.0.1 to 12.0.2 (with Chrome 89.0.4389.90)
* Old build detection: bump old build threshold from 60 to 90 days
43.0.0 / 2021-03-10
===================
* **[BREAKING]** Bump to Electron 12.0.1 with Chrome 89.0.4389.82
See https://www.electronjs.org/blog/electron-12-0
and https://www.electronjs.org/docs/breaking-changes#planned-breaking-api-changes-120
Noteworthy to Nativefier users:
* As usual, new Chrome, with potential improvements/regressions to websites you use
* Removed Flash support. If you still need flash, pass a <12 version to the `-e` flag
* Removed support for older x86 CPUs that do not have SSE3
* **[BREAKING]** Automatically consider known login pages as internal (fix #706) (PR #1124)
URLs for known login pages (e.g. `accounts.google.com` or `login.live.com`)
are now automatically considered internal, to let you login in your
Nativefier app without having to fiddle with `--internal-urls`.
This does not replace `internal-urls`, it complements it, and happens
_before_ your `internal-urls` rule is applied. So, if you already set
the flag to let such auth pages open internally, you can change it if
you want to clean it up, but it's not unnecessary.
We think this is desirable behavior and are so far unaware of cases
where users might not want this. If you disagree, please chime in at
[PR #1124: App: Automatically consider known login pages as internal](https://github.com/nativefier/nativefier/pull/1124)
* Various maintenance fixes: deps, scripts, slim down Docker size
42.4.0 / 2021-03-04
===================
* macOS: Prompt for accessibility permissions if needed by Global Shortcuts using Media Keys (fix #1120, PR #1121)
* Icon conversion: support GraphicsMagick in addition to ImageMagick (PR #1002)
* Docker: fix Windows builds, line endings, switch to Alpine (fix #997, PR #1122)
* Fix considering "same domain-ish" URLs as internal (PR #1126)
This was a regression introduced in 42.3.0 by dropping `wurl` in 6b266b781.
The new behavior is super close to 42.2.1. So, not considering it breaking.
* Various maintenance fixes: tooling, deps, CI
42.3.0 / 2021-02-25
===================
* Bump default Electron to 11.3.0 (with Chromium 87.0.4280.141).
macOS-segfault-causing icon bug #1101 should remain fixed.
* API docs: fix typo in option "-v" (PR #1114)
* Get rid of dep `shelljs` and abandoned app dep `wurl`
* Bump commander from 4 to 7 and eslint-config-prettier from 7 to 8
42.2.1 / 2021-01-30
===================
* Move GitHub repository to [`nativefier/nativefier`](https://github.com/nativefier/nativefier)
* Temporarily increase timeout for network call in test
* Move TS @types from dependencies to devDependencies (PR #1102)
42.2.0 / 2021-01-18
===================
* Revert default Electron back to 11.1.1 (Chrome 87.0.4280.88) (fix #1101)
11.2.0 was causing segfaults in macOS.
42.1.0 / 2021-01-16
===================
* Bump default Electron to 11.2.0 (with Chromium 87.0.4280.141)
* Maintenance:
* A bit more filename & appname sanitization
* Get rid of two direct deps: cheerio, lodash
* Fix error surfacing in full since recent changes in `page-icon`
* Publish TS types, for them to show up in npm
42.0.2 / 2020-12-07
===================
* Fix arg validation regression in #1080 with `--{x,y}` (fix #1084)
42.0.1 / 2020-12-06 42.0.1 / 2020-12-06
=================== ===================
@ -11,7 +587,7 @@ This release includes several contributor patches. Thanks @sorhtyre @mattruzzi !
* **[BREAKING CHANGE] Warn on old Electron/Chrome (fix #556) (PR #1076)** * **[BREAKING CHANGE] Warn on old Electron/Chrome (fix #556) (PR #1076)**
⚠️ Users packaging kiosk apps running for a long time on internal websites, ⚠️ Users packaging kiosk apps running for a long time on internal websites,
see https://github.com/jiahaog/nativefier/blob/master/docs/api.md#disable-old-build-warning-yesiknowitisinsecure see https://github.com/nativefier/nativefier/blob/master/API.md#disable-old-build-warning-yesiknowitisinsecure
* Check for improperly-formatted arguments (fix #885) (PR #1080) * Check for improperly-formatted arguments (fix #885) (PR #1080)
* Correctly start in tray when both `--maximize` and `--tray start-in-tray` are passed (fix #1015) (PR #1079) * Correctly start in tray when both `--maximize` and `--tray start-in-tray` are passed (fix #1015) (PR #1079)
* Fix icon path error when passing asar `--conceal` flag (fix #975) (PR #1074) * Fix icon path error when passing asar `--conceal` flag (fix #975) (PR #1074)
@ -110,7 +686,7 @@ Except *now*, as I have a breaking change, which would bump Nativefier to
================== ==================
* Attempt to fix failing to install due to app yarn install (#923) * Attempt to fix failing to install due to app yarn install (#923)
See https://github.com/jiahaog/nativefier/pull/898#issuecomment-583865045 . See https://github.com/nativefier/nativefier/pull/898#issuecomment-583865045 .
8.0.2 / 2020-03-15 8.0.2 / 2020-03-15
================== ==================
@ -134,7 +710,7 @@ Revamp and move to TypeScript (#898)
- That's it. Lots of care went into breaking CLI & programmatic behavior - That's it. Lots of care went into breaking CLI & programmatic behavior
as little as possible. **Please report regressions**. as little as possible. **Please report regressions**.
- Known issue: build may fail behind a proxy. Get in touch if you use one: - Known issue: build may fail behind a proxy. Get in touch if you use one:
https://github.com/jiahaog/nativefier/issues/907#issuecomment-596144768 https://github.com/nativefier/nativefier/issues/907#issuecomment-596144768
## Changes summary ## Changes summary
@ -181,7 +757,7 @@ while **not changing the CLI/programmatic interfaces**. Highlights:
Bugfixes will come later. Still, these got addressed: Bugfixes will come later. Still, these got addressed:
- Add common `Alt`+`Left`/`Right` for previous/next navigation. - Add common `Alt`+`Left`/`Right` for previous/next navigation.
- Improve #379: fix zoom with `Ctrl` + numpad `+`/`-` - Improve #379: fix zoom with `Ctrl` + numpad `+`/`-`
- Fix pinch-to-zoom (see https://github.com/jiahaog/nativefier/issues/379#issuecomment-598612128 ) - Fix pinch-to-zoom (see https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 )
7.7.1 / 2020-01-23 7.7.1 / 2020-01-23
@ -611,7 +1187,7 @@ while **not changing the CLI/programmatic interfaces**. Highlights:
* Make debug script automatically open the packaged app on OSX * Make debug script automatically open the packaged app on OSX
* Remove "About Electron" from app menu, add nativefier version to help, which fixes #18 * Remove "About Electron" from app menu, add nativefier version to help, which fixes #18
* Implement `--pretend` flag to easily simulate user agent strings, fixes #11 * Implement `--pretend` flag to easily simulate user agent strings, fixes #11
* Merge branch 'master' of github.com:jiahaog/nativefier * Merge branch 'master' of github.com:nativefier/nativefier
* Fix bug in error when response is undefined * Fix bug in error when response is undefined
* Add helper scripts to debug easily * Add helper scripts to debug easily
* Hide app instead of exiting on OSX to fix #14 * Hide app instead of exiting on OSX to fix #14

View File

@ -1,34 +1,55 @@
FROM node:12-stretch FROM --platform=linux/amd64 node:lts-alpine
LABEL description="Debian image to build nativefier apps" LABEL description="Alpine image to build Nativefier apps"
# Get wine32, not 64, to work around binary incompatibility with rcedit.
# https://github.com/jiahaog/nativefier/issues/375#issuecomment-304247033
# Forced us to use Debian rather than Alpine, which doesn't do multiarch.
RUN dpkg --add-architecture i386
# Install dependencies # Install dependencies and cleanup extraneous files
RUN apt-get update \ RUN apk update \
&& apt-get --yes install wine32 imagemagick \ && apk add bash wine imagemagick dos2unix \
&& apt-get clean && rm -rf /var/lib/apt/lists/* && rm -rf /var/cache/apk/* \
&& mkdir /nativefier && chown node:node /nativefier
# Add sources # Use node (1000) as default user not root
COPY . /nativefier USER node
ENV NPM_PACKAGES="/home/node/npm-packages"
ENV PATH="$PATH:$NPM_PACKAGES/bin"
ENV MANPATH="$MANPATH:$NPM_PACKAGES/share/man"
# Setup a global packages location for "node" user so we can npm link
RUN mkdir $NPM_PACKAGES \
&& npm config set prefix $NPM_PACKAGES
# Build nativefier and link globally
WORKDIR /nativefier/app
RUN npm install
WORKDIR /nativefier WORKDIR /nativefier
RUN npm install && npm run build && npm link
# Use 1000 as default user not root # Add sources with node as the owner so that it has the power it needs to build in /nativefier
USER 1000 COPY --chown=node:node . .
# Run a {lin,mac,win} build: 1. to check installation was sucessful, # Fix line endings that may have gotten mangled in Windows
# 2. to cache electron distributables and avoid downloads at runtime. RUN find ./icon-scripts ./src ./app -type f -print0 | xargs -0 dos2unix
RUN nativefier https://github.com/jiahaog/nativefier /tmp/nativefier \
&& nativefier -p osx https://github.com/jiahaog/nativefier /tmp/nativefier \ # Link (which will install and build)
&& nativefier -p windows https://github.com/jiahaog/nativefier /tmp/nativefier \ # Run tests (to ensure we don't Docker build & publish broken stuff)
# Cleanup leftover files in this step to not waste Docker layer space
# Make sure nativefier is executable
RUN npm i \
&& npm link \
&& npm run test:noplaywright \
&& rm -rf /tmp/nativefier* ~/.npm/_cacache ~/.cache/electron \
&& chmod +x $NPM_PACKAGES/bin/nativefier
# Run a {lin,mac,win} build
# 1. to check installation was sucessful
# 2. to cache electron distributables and avoid downloads at runtime
# Also delete generated apps so they don't get added to the Docker layer
# !Important! The `rm -rf` command must be in the same `RUN` command (using an `&&`), to not waste Docker layer space
RUN nativefier https://github.com/nativefier/nativefier /tmp/nativefier \
&& nativefier -p osx https://github.com/nativefier/nativefier /tmp/nativefier \
&& nativefier -p windows https://github.com/nativefier/nativefier /tmp/nativefier \
&& rm -rf /tmp/nativefier && rm -rf /tmp/nativefier
RUN echo Generated Electron cache size: $(du -sh ~/.cache/electron) \
&& echo Final image size: $(du -sh / 2>/dev/null)
ENTRYPOINT ["nativefier"] ENTRYPOINT ["nativefier"]
CMD ["--help"] CMD ["--help"]

255
HACKING.md Normal file
View File

@ -0,0 +1,255 @@
# Development Guide
Welcome, soon-to-be contributor 🙂! This document sums up
what you need to know to get started hacking on Nativefier.
## Guidelines
1. **Before starting work on a huge change, gauge the interest**
of community & maintainers through a GitHub issue. For big changes,
create a **[RFC](https://en.wikipedia.org/wiki/Request_for_Comments)**
issue to enable a good peer review.
2. Do your best to **avoid adding new Nativefier command-line options**.
If a new option is inevitable for what you want to do, sure,
but as much as possible try to see if you change works without.
Nativefier already has a ton of them, making it hard to use.
3. Do your best to **limit breaking changes**.
Only introduce breaking changes when necessary, when required by deps, or when
not breaking would be unreasonable. When you can, support the old thing forever.
For example, keep maintaining old flags; to "replace" an flag you want to replace
with a better version, you should keep honoring the old flag, and massage it
to pass parameters to the new flag, maybe using a wrapper/adapter.
Yes, our code will get a tiny bit uglier than it could have been with a hard
breaking change, but that would be to ignore our users.
Introducing breaking changes willy nilly is a comfort to us developers, but is
disrespectful to end users who must constantly bend to the flow of breaking changes
pushed by _all their software_ who think it's "just one breaking change".
See [Rich Hickey - Spec-ulation](https://www.youtube.com/watch?v=oyLBGkS5ICk).
4. **Avoid adding npm dependencies**. Each new dep is a complexity & security liability.
You might be thinking your extra dep is _"just a little extra dep"_, and maybe
you found one that is high-quality & dependency-less. Still, it's an extra dep,
and over the life of Nativefier we requested changes to _dozens_ of PRs to avoid
"just a little extra dep". Without this constant attention, Nativefier would be
more bloated, less stable for users, more annoying to maintainers. Now, don't go
rewriting zlib if you need a zlib dep, for sure use a dep. But if you can write a
little helper function saving us a dep for a mundane task, go for the helper :) .
Also, an in-tree helper will always be less complex than a dep, as inherently
more tailored to our use case, and less complexity is good.
5. Use **types**, avoid `any`, write **tests**.
6. **Document for users** in `API.md`
7. **Document for other devs** in comments, jsdoc, commits, PRs.
Say _why_ more than _what_, the _what_ is your code!
## Setup
First, clone the project:
```bash
git clone https://github.com/nativefier/nativefier.git
cd nativefier
```
Install dependencies (for both the CLI and the Electron app):
```bash
npm ci
```
The above `npm ci` will build automatically (through the `prepare` hook).
When you need to re-build Nativefier,
```bash
npm run build
```
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, you can run Nativefier with your test parameters:
```bash
nativefier --your-awesome-new-flag 'https://your-test-site.com'
```
Then run your nativefier app _through the command line too_ (to see logs & errors):
```bash
# Under Linux
./your-test-site-linux-x64/your-test-site
# Under Windows
your-test-site-win32-x64/your-test-site.exe
# Under macOS
./YourTestSite-darwin-x64/YourTestSite.app/Contents/MacOS/YourTestSite --verbose
```
## 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
- 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:
1. Run a TSC watcher: `npm run build:watch`
2. Run a Jest unit tests watcher: `npm run test:watch`
3. Here is [a screencast of how the live-reload experience should look like](https://user-images.githubusercontent.com/522085/120407694-abdf3f00-c31b-11eb-9ab5-a531a929adb9.mp4)
- Alternatively, you can run both test processes in the same terminal by running: `npm run watch`
## Maintainers corner
### Deps: major-upgrading Electron
When a new major [Electron release](https://github.com/electron/electron/releases) occurs,
1. Wait a few weeks to let it stabilize. Never upgrade Nativefier to a `.0.0`.
2. Thoroughly digest the new version's [breaking changes](https://www.electronjs.org/docs/breaking-changes)
(also via the [Releases page](https://github.com/electron/electron/releases) and [the blog](https://www.electronjs.org/blog/), the content is different),
grepping our codebase for every changed API.
- If called for by the breaking changes, perform the necessary API changes
3. Bump
- `src/constants.ts` / `DEFAULT_ELECTRON_VERSION` & `DEFAULT_CHROME_VERSION`
- `package.json / devDeps / electron`
- `app / package.json / devDeps / electron`
4. On Windows, macOS, Linux, test for regression and crashes:
1. With `npm test` and `npm run test:manual`
2. With extra manual testing
5. When confident enough, release it in a regression-spelunking-friendly way:
1. If `master` has unreleased commits, make a patch/minor release with them, but without the major Electron bump.
2. Commit your Electron major bump and release it as a major new Nativefier version. Help users identify the breaking change by using a bold **[BREAKING]** marker in `CHANGELOG.md` and in the GitHub release.
### Deps updates
It is important to stay afloat of dependencies upgrades.
In packages ecosystems like npm, there's only one way: forward.
The best time to do package upgrades is now / progressively, because:
1. Slacking on doing these upgrades means you stay behind, and it becomes
risky to do them. Upgrading a woefully out-of-date dep from 3.x to 9.x is
scarier than 3.x to 4.x, release, then 4.x to 5.x, release, etc... to 9.x.
2. Also, dependencies applying security patches to old major versions are rare
in npm. So, by slacking on upgrades, it becomes more and more probable that
we get impacted by a vulnerability. And when this happens, it then becomes
urgent & stressful to A. fix the vulnerability, B. do the required major upgrades.
So: do upgrade CLI & App deps regularly! Our release script will remind you about it.
### Deps lockfile / shrinkwrap
We do use lockfiles (`npm-shrinkwrap.json` & `app/npm-shrinkwrap.json`), for:
1. Security (avoiding supply chain attacks)
2. Reproducibility
3. Performance
It means you might have to update these lockfiles when adding a dependency.
`npm run relock` will help you with that.
Note: we do use `npm-shrinkwrap.json` rather than `package-lock.json` because
the latter is tailored to libraries, and is not publishable.
As [documented](https://docs.npmjs.com/cli/v6/configuring-npm/shrinkwrap-json),
CLI tools like Nativefier should use shrinkwrap.
### Release
While on `master`, with no uncommitted changes, run:
```bash
npm run changelog -- $VERSION
# With no 'v'. For example: npm run changelog -- '42.5.0'
```
Do follow semantic versioning, and give visibility to breaking changes
in release notes by prefixing their line with **[BREAKING]**.
### Triage
These are the guidelines we (try to) follow when triaging [issues](https://github.com/nativefier/nativefier/issues):
1. Do your best to conciliate **empathy & efficiency, and keep your cool**.
Its not always easy 😄😬😭🤬. Get away from triaging if you feel grouchy.
2. **Rename** issues. Most issues are badly named, with titles ranging from
unclear to flat out wrong. A good backlog is a backlog of issues with clear
concise titles, understandable with only the title after you read them once.
Rename and clarify.
3. **Ask for clarification & details** when needed, and add a `need-info` label.
1. In particular, if the issue isnt reproducible (e.g. a non-trivial bug
happening on an internal site), express that we cant work without a
repro scenario, and flag as `need-info`.
4. **Label** issues with _category/sorting_ labels (e.g. `mac` / `linux` / `windows`,
`bug` / `feature-request` ...) and _status_ labels (e.g. `upstream`, `wontfix`,
`need-info`, `cannot-reproduce`).
5. **Close if needed, but not too much**. We _do_ want to close what deserves it,
but closing _too_ ruthlessly frustrates and disappoints users, and does us a
disservice of not having a clear honest backlog available to us & users. So,
1. When in doubt, leave issues open and triaged as `bug` / `feature-request`.
Its okay, reaching 0 open issues is _not_ an objective. Or if it is,
it deserves to be a development objective, not a triage one.
2. That being said, do close whats `upstream`, with a kind message.
3. Also do close bugs that have been `need-info` or `cannot-reproduce` for
too long (weeks / months), with a kind message explaining were okay to
re-open if the requested info / scenario is provided.
4. Finally, carefully close issues we do not want to address, e.g. requests
going against project goals, or bugs & feature requests that are so niche
or far-fetched that theres zero chance of ever seeing them addressed.
But if in doubt, remain at point 1. above: leave open, renamed, labelled.
6. **Close duplicates issues** and link to the original issue.
1. To be able to notice dups implies you must know the backlog (one more
reason to keep it tidy and palatable). Once in a blue moon, do a
"full pass" of the whole backlog from beginning to end, youll often
find lots of now-irrelevant bugs, and duplicates.
7. **Use [GitHub saved replies](https://github.com/settings/replies)** to
automate asking for info and being nice on closing as noanswer / stale-needinfo.
8. **Transform findings stemming from issues discussion** into documentation
(chiefly, [CATALOG.md](CATALOG.md) & [API.md](API.md)), or into code comments.
9. **Dont scold authors of lame "+1" comments**, this only adds to the noise
youre trying to avoid. Instead, hide useless comments as `Off-topic`.
From personal experience, users do understand this signal, and such hidden
comments do avoid an avalanche of extra "+1" comments.
1. There are shades of lame. A literal `"+1"` comment is frankly useless and
is worth hiding. But a comment like `"same for me on Windows"` at least
brings an extra bit of information, so can remain visible.
2. In a perfect world, GitHub would let us add a note when hiding comments to
express _"Please use a 👍 reaction on the issue to vote for it instead of_
_posting a +1 comment"_. In a perfecter world, GitHub would use their AI
skillz to automatically detect such comments, discourage them and nudge
towards a 👍 reaction. Were not there yet, so “hidden as off-topic” will do.
10. **Dont let yourself be abused** by abrasive / entitled users. There are
plenty of articles documenting open-source burnout and trolls-induced misery.
Find an article that speaks to you, and point problematic users to it.
I like [Brett Cannon - The social contract of open source](https://snarky.ca/the-social-contract-of-open-source/).

125
README.md
View File

@ -1,90 +1,101 @@
Note: Nativefier is unmaintained, please see https://github.com/nativefier/nativefier/issues/1577.
# Nativefier # Nativefier
[![Build Status](https://github.com/jiahaog/nativefier/workflows/ci/badge.svg)](https://github.com/jiahaog/nativefier/actions?query=workflow%3Aci) ![Example of Nativefier app in the macOS dock](.github/dock-screenshot.png)
[![npm version](https://badge.fury.io/js/nativefier.svg)](https://www.npmjs.com/package/nativefier)
![Dock](docs/dock.png) You want to make a native-looking wrapper for WhatsApp Web (or any web page).
You want to make a native wrapper for WhatsApp Web (or any web page).
```bash ```bash
nativefier web.whatsapp.com nativefier 'web.whatsapp.com'
``` ```
![Walkthrough animation](docs/walkthrough.gif) ![Walkthrough animation](.github/nativefier-walkthrough.gif)
You're done. You're done.
## Introduction ## Introduction
Nativefier is a command-line tool to easily create a desktop app for any web site Nativefier is a command-line tool to easily create a desktop app for any web site
with minimal configuration. Apps are wrapped by [Electron](https://www.electronjs.org/) with minimal fuss. Apps are wrapped by [Electron](https://www.electronjs.org/)
(which uses Chromium under the hood) in an OS executable (`.app`, `.exe`, etc) (which uses Chromium under the hood) in an OS executable (`.app`, `.exe`, etc)
for use on Windows, macOS and Linux. usable 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 I built this because I grew tired of having to Alt-Tab to my browser and then search
through the numerous open tabs when I was using [Facebook Messenger](https://messenger.com) or through numerous open tabs when using Messenger or
[Whatsapp Web](https://web.whatsapp.com) ([HN thread](https://news.ycombinator.com/item?id=10930718)). Nativefier features: Whatsapp Web ([HN thread](https://news.ycombinator.com/item?id=10930718)). Nativefier features:
- Automatically retrieval of app icon / name. - Automatically retrieval of app icon / name
- JavaScript and CSS injection. - Injection of custom JS & CSS
- Many more, see the [API docs](docs/api.md) or `nativefier --help` - Many more, see the [API docs](API.md) or `nativefier --help`
## Installation ## Installation
- macOS 10.9+ / Windows / Linux Install Nativefier globally with `npm install -g nativefier` . Requirements:
- [Node.js](https://nodejs.org/) `>= 10` and npm `>= 6`
- Optional dependencies:
- [ImageMagick](http://www.imagemagick.org/) to convert icons.
Make sure `convert` and `identify` are in your system `$PATH`.
- [Wine](https://www.winehq.org/) to package Windows apps under non-Windows platforms.
Make sure `wine` is in your system `$PATH`.
Then, install Nativefier globally with `npm install -g nativefier` - macOS 10.13+ / Windows / Linux
- [Node.js](https://nodejs.org/) ≥ 16.9 and npm ≥ 7.10
Optional dependencies:
- [ImageMagick](http://www.imagemagick.org/) or [GraphicsMagick](http://www.graphicsmagick.org/) to convert icons.
Be sure `convert` + `identify` or `gm` are in your `$PATH`.
- [Wine](https://www.winehq.org/) to build Windows apps from non-Windows platforms.
Be sure `wine` is in your `$PATH`.
<details>
<summary>Or install with Docker (click to expand)</summary>
- Pull the image from [Docker Hub](https://hub.docker.com/r/nativefier/nativefier): `docker pull nativefier/nativefier`
- ... or build it yourself: `docker build -t local/nativefier .`
(in this case, replace `nativefier/` in the below examples with `local/`)
By default, `nativefier --help` will be executed.
To build e.g. a Gmail app into `~/nativefier-apps`,
```bash
docker run --rm -v ~/nativefier-apps:/target/ nativefier/nativefier https://mail.google.com/ /target/
```
You can pass Nativefier flags, and mount volumes to pass local files. E.g. to use an icon,
```bash
docker run --rm -v ~/my-icons-folder/:/src -v $TARGET-PATH:/target nativefier/nativefier --icon /src/icon.png --name whatsApp -p linux -a x64 https://web.whatsapp.com/ /target/
```
</details>
<details>
<summary>Or install with Snap & AUR (click to expand)</summary>
These repos are *not* managed by Nativefier maintainers; use at your own risk.
If using them, for your security, please inspect the build script.
- [Snap](https://snapcraft.io/nativefier)
- [AUR](https://aur.archlinux.org/packages/nodejs-nativefier)
</details>
## Usage ## Usage
To create a native desktop app for [medium.com](https://medium.com), To create an app for medium.com, simply `nativefier 'medium.com'`
simply `nativefier "medium.com"`
Nativefier will try to determine the app name, and well as lots of other options. Nativefier will try to determine the app name, and well as other options that you
If desired, these options can be overwritten. For example, to override the name, can override. For example, to override the name, `nativefier --name 'My Medium App' 'medium.com'`
`nativefier --name 'My Medium App' 'medium.com'`
**Read the [API documentation](docs/api.md) or run `nativefier --help`** **Read the [API docs](API.md) or run `nativefier --help`**
to learn about other command-line flags usable to configure the packaged app. to learn about command-line flags and configure your app.
To have high-resolution icons used by default for an app/domain, please ## Troubleshooting
contribute to the [icon repository](https://github.com/jiahaog/nativefier-icons)!
## Usage with Docker **See [CATALOG.md](CATALOG.md) for site-specific ideas & workarounds contributed by the community**.
Nativefier is also usable from Docker. If this doesnt help, go look at our [issue tracker](https://github.com/nativefier/nativefier/issues).
- Pull the latest stable image from Docker Hub: `docker pull jiahaog/nativefier`
- ... or build the image yourself: `docker build -t local/nativefier .`
(in this case, replace `jiahaog/` in the below examples with `local/`)
By default, the command `nativefier --help` will be executed.
To build e.g. a Gmail nativefier app to a writable local `~/nativefier-apps`,
```bash
docker run --rm -v ~/nativefier-apps:/target/ jiahaog/nativefier https://mail.google.com/ /target/
```
You can pass Nativefier flags, and mount volumes to provide local files. For example, to use an icon,
```bash
docker run --rm -v ~/my-icons-folder/:/src -v $TARGET-PATH:/target jiahaog/nativefier --icon /src/icon.png --name whatsApp -p linux -a x64 https://web.whatsapp.com/ /target/
```
## Development ## Development
Help welcome on [bugs](https://github.com/jiahaog/nativefier/issues?q=is%3Aopen+is%3Aissue+label%3Abug) and Help welcome on [bugs](https://github.com/nativefier/nativefier/issues?q=is%3Aopen+is%3Aissue+label%3Abug) and
[feature requests](https://github.com/jiahaog/nativefier/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request). [feature requests](https://github.com/nativefier/nativefier/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request)!
[Developer / build docs](docs/development.md), [API documentation](docs/api.md), Docs: [Developer / build / hacking](HACKING.md), [API / flags](API.md),
[Changelog](CHANGELOG.md). [Changelog](CHANGELOG.md).
## License License: [MIT](LICENSE.md).
[MIT](LICENSE.md)

View File

@ -1,28 +1,21 @@
const base = require('../base-eslintrc');
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md // # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
module.exports = { module.exports = {
parser: '@typescript-eslint/parser', parser: base.parser,
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json'],
}, },
plugins: ['@typescript-eslint'], plugins: base.plugins,
extends: [ extends: base.extends,
'eslint:recommended', rules: base.rules,
'prettier', // https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files
'plugin:@typescript-eslint/eslint-recommended', ignorePatterns: [
'plugin:@typescript-eslint/recommended', 'node_modules/**',
'plugin:@typescript-eslint/recommended-requiring-type-checking', 'lib/**',
'dist/**',
'built-tests/**',
'coverage/**',
], ],
rules: {
'prettier/prettier': 'error',
// TODO remove when done killing `any`s and making tsc strict
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-template-expressions': 'off'
},
}; };

View File

@ -1 +0,0 @@
package-lock=false

1170
app/npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,14 +12,14 @@
], ],
"scripts": {}, "scripts": {},
"dependencies": { "dependencies": {
"electron-context-menu": "2.x", "electron-context-menu": "^3.6.1",
"electron-dl": "3.x", "electron-dl": "^3.5.0",
"electron-squirrel-startup": "1.x", "electron-squirrel-startup": "^1.0.0",
"electron-window-state": "5.x", "electron-window-state": "^5.0.3",
"source-map-support": "0.x", "loglevel": "^1.8.1",
"wurl": "2.x" "source-map-support": "^0.5.21"
}, },
"devDependencies": { "devDependencies": {
"electron": "11.x" "electron": "^25.7.0"
} }
} }

View File

@ -1,33 +1,84 @@
import { shell } from 'electron'; import {
BrowserWindow,
ContextMenuParams,
Event as ElectronEvent,
} from 'electron';
import contextMenu from 'electron-context-menu'; import contextMenu from 'electron-context-menu';
export function initContextMenu(createNewWindow, createNewTab): void { import { nativeTabsSupported, openExternal } from '../helpers/helpers';
import * as log from '../helpers/loggingHelper';
import { setupNativefierWindow } from '../helpers/windowEvents';
import { createNewWindow } from '../helpers/windowHelpers';
import {
OutputOptions,
outputOptionsToWindowOptions,
} from '../../../shared/src/options/model';
export function initContextMenu(
options: OutputOptions,
window?: BrowserWindow,
): void {
log.debug('initContextMenu');
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
contextMenu({ contextMenu({
prepend: (actions, params) => { prepend: (actions: contextMenu.Actions, params: ContextMenuParams) => {
log.debug('contextMenu.prepend', { actions, params });
const items = []; const items = [];
if (params.linkURL) { if (params.linkURL && window) {
items.push({ items.push({
label: 'Open Link in Default Browser', label: 'Open Link in Default Browser',
click: () => { click: () => {
shell.openExternal(params.linkURL); // eslint-disable-line @typescript-eslint/no-floating-promises openExternal(params.linkURL).catch((err) =>
log.error('contextMenu Open Link in Default Browser ERROR', err),
);
}, },
}); });
items.push({ items.push({
label: 'Open Link in New Window', label: 'Open Link in New Window',
click: () => { click: () =>
createNewWindow(params.linkURL); createNewWindow(
}, outputOptionsToWindowOptions(options, nativeTabsSupported()),
setupNativefierWindow,
params.linkURL,
// window,
),
}); });
if (createNewTab) { if (nativeTabsSupported()) {
items.push({ items.push({
label: 'Open Link in New Tab', label: 'Open Link in New Tab',
click: () => { click: () =>
createNewTab(params.linkURL, false); // // Fire a new window event for a foreground tab
}, // // Previously we called createNewTab directly, but it had incosistent and buggy behavior
// // as it was mostly designed for running off of events. So this will create a new event
// // for a foreground-tab for the event handler to grab and take care of instead.
// (window as BrowserWindow).webContents.emit(
// // event name
// 'new-window',
// // event object
// {
// // Leave to the default for a NewWindowWebContentsEvent
// newGuest: undefined,
// ...new Event('new-window'),
// }, // as NewWindowWebContentsEvent,
// // url
// params.linkURL,
// // frameName
// window?.webContents.mainFrame.name ?? '',
// // disposition
// 'foreground-tab',
// ),
window.emit('new-window-for-tab', {
...new Event('new-window-for-tab'),
url: params.linkURL,
} as ElectronEvent<{ url: string }>),
}); });
} }
} }
return items; return items;
}, },
showCopyImage: true,
showCopyImageAddress: true,
showSaveImage: true,
}); });
} }

View File

@ -2,20 +2,36 @@ import * as path from 'path';
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
export function createLoginWindow(loginCallback): BrowserWindow { import * as log from '../helpers/loggingHelper';
import { nativeTabsSupported } from '../helpers/helpers';
export async function createLoginWindow(
loginCallback: (username?: string, password?: string) => void,
parent?: BrowserWindow,
): Promise<BrowserWindow> {
log.debug('createLoginWindow', {
loginCallback,
parent,
});
const loginWindow = new BrowserWindow({ const loginWindow = new BrowserWindow({
parent: nativeTabsSupported() ? undefined : parent,
width: 300, width: 300,
height: 400, height: 400,
frame: false, frame: false,
resizable: false, resizable: false,
webPreferences: { webPreferences: {
nodeIntegration: true, // TODO work around this; insecure nodeIntegration: true, // TODO work around this; insecure
contextIsolation: false, // https://github.com/electron/electron/issues/28017
sandbox: false, // https://www.electronjs.org/blog/electron-20-0#default-changed-renderers-without-nodeintegration-true-are-sandboxed-by-default
}, },
}); });
// eslint-disable-next-line @typescript-eslint/no-floating-promises await loginWindow.loadURL(
loginWindow.loadURL(`file://${path.join(__dirname, 'static/login.html')}`); `file://${path.join(__dirname, 'static/login.html')}`,
);
ipcMain.once('login-message', (event, usernameAndPassword) => { ipcMain.once('login-message', (event, usernameAndPassword: string[]) => {
log.debug('login-message', { event, username: usernameAndPassword[0] });
loginCallback(usernameAndPassword[0], usernameAndPassword[1]); loginCallback(usernameAndPassword[0], usernameAndPassword[1]);
loginWindow.close(); loginWindow.close();
}); });

View File

@ -1,109 +1,68 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { BrowserWindow, shell, ipcMain, dialog, Event } from 'electron'; import {
desktopCapturer,
ipcMain,
BrowserWindow,
Event,
HandlerDetails,
} from 'electron';
import windowStateKeeper from 'electron-window-state'; import windowStateKeeper from 'electron-window-state';
import {
isOSX,
linkIsInternal,
getCssToInject,
shouldInjectCss,
getAppIcon,
nativeTabsSupported,
getCounterValue,
} from '../helpers/helpers';
import { initContextMenu } from './contextMenu'; import { initContextMenu } from './contextMenu';
import { onNewWindowHelper } from './mainWindowHelpers';
import { createMenu } from './menu'; import { createMenu } from './menu';
import {
getAppIcon,
getCounterValue,
isOSX,
nativeTabsSupported,
} from '../helpers/helpers';
import * as log from '../helpers/loggingHelper';
import { IS_PLAYWRIGHT } from '../helpers/playwrightHelpers';
import { onNewWindow, setupNativefierWindow } from '../helpers/windowEvents';
import {
clearCache,
createNewTab,
getDefaultWindowOptions,
hideWindow,
} from '../helpers/windowHelpers';
import {
OutputOptions,
outputOptionsToWindowOptions,
} from '../../../shared/src/options/model';
const ZOOM_INTERVAL = 0.1; export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
function hideWindow( type SessionInteractionRequest = {
window: BrowserWindow, id?: string;
event: Event, func?: string;
fastQuit: boolean, funcArgs?: unknown[];
tray, property?: string;
): void { propertyValue?: unknown;
if (isOSX() && !fastQuit) { };
// this is called when exiting from clicking the cross button on the window
event.preventDefault();
window.hide();
} else if (!fastQuit && tray) {
event.preventDefault();
window.hide();
}
// will close the window on other platforms
}
function injectCss(browserWindow: BrowserWindow): void { type SessionInteractionResult<T = unknown> = {
if (!shouldInjectCss()) { id?: string;
return; value?: T | Promise<T>;
} error?: Error;
};
const cssToInject = getCssToInject();
browserWindow.webContents.on('did-navigate', () => {
// 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
(details, callback) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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 {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
browserWindow.webContents.session.setProxy({
proxyRules,
pacScript: '',
proxyBypassRules: '',
});
}
/** /**
* @param {{}} nativefierOptions AppArgs from nativefier.json * @param {{}} nativefierOptions AppArgs from nativefier.json
* @param {function} onAppQuit
* @param {function} setDockBadge * @param {function} setDockBadge
*/ */
export function createMainWindow( export async function createMainWindow(
nativefierOptions, nativefierOptions: OutputOptions,
onAppQuit, setDockBadge: (value: number | string, bounce?: boolean) => void,
setDockBadge, ): Promise<BrowserWindow> {
): BrowserWindow {
const options = { ...nativefierOptions }; const options = { ...nativefierOptions };
const mainWindowState = windowStateKeeper({ const mainWindowState = windowStateKeeper({
defaultWidth: options.width || 1280, defaultWidth: options.width || 1280,
defaultHeight: options.height || 800, defaultHeight: options.height || 800,
}); });
const DEFAULT_WINDOW_OPTIONS = {
// Convert dashes to spaces because on linux the app name is joined with dashes
title: options.name,
tabbingIdentifier: nativeTabsSupported() ? options.name : undefined,
webPreferences: {
javascript: true,
plugins: true,
nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com
webSecurity: !options.insecure,
preload: path.join(__dirname, 'preload.js'),
zoomFactor: options.zoom,
},
};
const browserwindowOptions = { ...options.browserwindowOptions };
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
frame: !options.hideWindowFrame, frame: !options.hideWindowFrame,
width: mainWindowState.width, width: mainWindowState.width,
@ -116,241 +75,149 @@ export function createMainWindow(
y: options.y, y: options.y,
autoHideMenuBar: !options.showMenuBar, autoHideMenuBar: !options.showMenuBar,
icon: getAppIcon(), icon: getAppIcon(),
// set to undefined and not false because explicitly setting to false will disable full screen fullscreen: options.fullScreen,
fullscreen: options.fullScreen || undefined,
// Whether the window should always stay on top of other windows. Default is false. // Whether the window should always stay on top of other windows. Default is false.
alwaysOnTop: options.alwaysOnTop, alwaysOnTop: options.alwaysOnTop,
titleBarStyle: options.titleBarStyle, titleBarStyle: options.titleBarStyle ?? 'default',
show: options.tray !== 'start-in-tray', // Maximize window visual glitch on Windows fix
// We want a consistent behavior on all OSes, but Windows needs help to not glitch.
// So, we manually mainWindow.show() later, see a few lines below
show: options.tray !== 'start-in-tray' && process.platform !== 'win32',
backgroundColor: options.backgroundColor, backgroundColor: options.backgroundColor,
...DEFAULT_WINDOW_OPTIONS, ...getDefaultWindowOptions(
...browserwindowOptions, outputOptionsToWindowOptions(options, nativeTabsSupported()),
),
}); });
// Just load about:blank to start, gives playwright something to latch onto initially for testing.
if (IS_PLAYWRIGHT) {
await mainWindow.loadURL('about:blank');
}
mainWindowState.manage(mainWindow); mainWindowState.manage(mainWindow);
// after first run, no longer force maximize to be true // after first run, no longer force maximize to be true
if (options.maximize) { if (options.maximize) {
mainWindow.maximize(); mainWindow.maximize();
options.maximize = undefined; options.maximize = undefined;
try { saveAppArgs(options);
fs.writeFileSync(
path.join(__dirname, '..', 'nativefier.json'),
JSON.stringify(options),
);
} catch (err) {
// eslint-disable-next-line no-console
console.log(
`WARNING: Ignored nativefier.json rewrital (${(err as Error).toString()})`,
);
}
} }
if (options.tray === 'start-in-tray') { if (options.tray === 'start-in-tray') {
mainWindow.hide(); mainWindow.hide();
} else if (process.platform === 'win32') {
// See other "Maximize window visual glitch on Windows fix" comment above.
mainWindow.show();
} }
const withFocusedWindow = (block: (window: BrowserWindow) => void): void => { const windowOptions = outputOptionsToWindowOptions(
const focusedWindow = BrowserWindow.getFocusedWindow(); options,
if (focusedWindow) { nativeTabsSupported(),
return block(focusedWindow);
}
return undefined;
};
const adjustWindowZoom = (
window: BrowserWindow,
adjustment: number,
): void => {
window.webContents.zoomFactor = window.webContents.zoomFactor + adjustment;
};
const onZoomIn = (): void => {
withFocusedWindow((focusedWindow: BrowserWindow) =>
adjustWindowZoom(focusedWindow, ZOOM_INTERVAL),
); );
}; createMenu(options, mainWindow);
createContextMenu(options, mainWindow);
setupNativefierWindow(windowOptions, mainWindow);
const onZoomOut = (): void => { // Note it is important to add these handlers only to the *main* window,
withFocusedWindow((focusedWindow: BrowserWindow) => // else we run into weird behavior like opening tabs twice
adjustWindowZoom(focusedWindow, -ZOOM_INTERVAL), mainWindow.webContents.setWindowOpenHandler((details: HandlerDetails) => {
return onNewWindow(
windowOptions,
setupNativefierWindow,
details,
mainWindow,
); );
};
const onZoomReset = (): void => {
withFocusedWindow((focusedWindow: BrowserWindow) => {
focusedWindow.webContents.zoomFactor = options.zoom;
}); });
}; mainWindow.on('new-window-for-tab', (event?: Event<{ url?: string }>) => {
log.debug('mainWindow.new-window-for-tab', { event });
const clearAppData = async (): Promise<void> => { createNewTab(
const response = await dialog.showMessageBox(mainWindow, { windowOptions,
type: 'warning', setupNativefierWindow,
buttons: ['Yes', 'Cancel'], event?.url ?? options.targetUrl,
defaultId: 1, true,
title: 'Clear cache confirmation', // mainWindow,
message:
'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?',
});
if (response.response !== 0) {
return;
}
await clearCache(mainWindow);
};
const onGoBack = (): void => {
withFocusedWindow((focusedWindow) => {
focusedWindow.webContents.goBack();
});
};
const onGoForward = (): void => {
withFocusedWindow((focusedWindow) => {
focusedWindow.webContents.goForward();
});
};
const getCurrentUrl = (): void =>
withFocusedWindow((focusedWindow) => focusedWindow.webContents.getURL());
const onBlockedExternalUrl = (url: string) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
dialog.showMessageBox(mainWindow, {
message: `Cannot navigate to external URL: ${url}`,
type: 'error',
title: 'Navigation blocked',
});
};
const onWillNavigate = (event: Event, urlToGo: string): void => {
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
event.preventDefault();
if (options.blockExternalUrls) {
onBlockedExternalUrl(urlToGo);
} else {
shell.openExternal(urlToGo); // eslint-disable-line @typescript-eslint/no-floating-promises
}
}
};
const createNewWindow: (url: string) => BrowserWindow = (url: string) => {
const window = new BrowserWindow(DEFAULT_WINDOW_OPTIONS);
if (options.userAgent) {
window.webContents.userAgent = options.userAgent;
}
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); // eslint-disable-line @typescript-eslint/no-floating-promises
return window;
};
const createNewTab = (url: string, foreground: boolean): BrowserWindow => {
withFocusedWindow((focusedWindow) => {
const newTab = createNewWindow(url);
focusedWindow.addTabbedWindow(newTab);
if (!foreground) {
focusedWindow.focus();
}
return newTab;
});
return undefined;
};
const createAboutBlankWindow = (): BrowserWindow => {
const window = createNewWindow('about:blank');
window.hide();
window.webContents.once('did-stop-loading', () => {
if (window.webContents.getURL() === 'about:blank') {
window.close();
} else {
window.show();
}
});
return window;
};
const onNewWindow = (
event: Event & { newGuest?: any },
urlToGo: string,
frameName: string,
disposition,
): void => {
const preventDefault = (newGuest: any): void => {
event.preventDefault();
if (newGuest) {
event.newGuest = newGuest;
}
};
onNewWindowHelper(
urlToGo,
disposition,
options.targetUrl,
options.internalUrls,
preventDefault,
shell.openExternal.bind(this),
createAboutBlankWindow,
nativeTabsSupported,
createNewTab,
options.blockExternalUrls,
onBlockedExternalUrl,
); );
};
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
// eslint-disable-next-line @typescript-eslint/no-floating-promises
window.webContents.setVisualZoomLevelLimits(1, 3);
window.webContents.send('params', JSON.stringify(options));
}); });
};
const menuOptions = {
nativefierVersion: options.nativefierVersion,
appQuit: onAppQuit,
zoomIn: onZoomIn,
zoomOut: onZoomOut,
zoomReset: onZoomReset,
zoomBuildTimeValue: options.zoom,
goBack: onGoBack,
goForward: onGoForward,
getCurrentUrl,
clearAppData,
disableDevTools: options.disableDevTools,
};
createMenu(menuOptions);
if (!options.disableContextMenu) {
initContextMenu(
createNewWindow,
nativeTabsSupported() ? createNewTab : undefined,
);
}
if (options.userAgent) {
mainWindow.webContents.userAgent = options.userAgent;
}
if (options.proxyRules) {
setProxyRules(mainWindow, options.proxyRules);
}
injectCss(mainWindow);
sendParamsOnDidFinishLoad(mainWindow);
if (options.counter) { if (options.counter) {
mainWindow.on('page-title-updated', (e, title) => { setupCounter(options, mainWindow, setDockBadge);
} else {
setupNotificationBadge(options, mainWindow, setDockBadge);
}
ipcMain.on('notification-click', () => {
log.debug('ipcMain.notification-click');
mainWindow.show();
});
setupSessionInteraction(mainWindow);
setupSessionPermissionHandler(mainWindow);
if (options.clearCache) {
await clearCache(mainWindow);
}
setupCloseEvent(options, mainWindow);
return mainWindow;
}
function createContextMenu(
options: OutputOptions,
window: BrowserWindow,
): void {
if (!options.disableContextMenu) {
initContextMenu(options, window);
}
}
export function saveAppArgs(newAppArgs: OutputOptions): void {
try {
fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs, null, 2));
} catch (err: unknown) {
log.warn(
`WARNING: Ignored nativefier.json rewrital (${(err as Error).message})`,
);
}
}
function setupCloseEvent(options: OutputOptions, window: BrowserWindow): void {
window.on('close', (event: Event) => {
log.debug('mainWindow.close', event);
if (window.isFullScreen()) {
if (nativeTabsSupported()) {
window.moveTabToNewWindow();
}
window.setFullScreen(false);
window.once('leave-full-screen', (event: Event) =>
hideWindow(
window,
event,
options.fastQuit ?? false,
options.tray ?? 'false',
),
);
}
hideWindow(
window,
event,
options.fastQuit ?? false,
options.tray ?? 'false',
);
if (options.clearCache) {
clearCache(window).catch((err) => log.error('clearCache ERROR', err));
}
});
}
function setupCounter(
options: OutputOptions,
window: BrowserWindow,
setDockBadge: (value: number | string, bounce?: boolean) => void,
): void {
window.on('page-title-updated', (event, title) => {
log.debug('mainWindow.page-title-updated', { event, title });
const counterValue = getCounterValue(title); const counterValue = getCounterValue(title);
if (counterValue) { if (counterValue) {
setDockBadge(counterValue, options.bounce); setDockBadge(counterValue, options.bounce);
@ -358,64 +225,112 @@ export function createMainWindow(
setDockBadge(''); setDockBadge('');
} }
}); });
} else { }
function setupSessionPermissionHandler(window: BrowserWindow): void {
window.webContents.session.setPermissionCheckHandler(() => {
return true;
});
window.webContents.session.setPermissionRequestHandler(
(_webContents, _permission, callback) => {
callback(true);
},
);
ipcMain.handle('desktop-capturer-get-sources', () => {
return desktopCapturer.getSources({
types: ['screen', 'window'],
});
});
}
function setupNotificationBadge(
options: OutputOptions,
window: BrowserWindow,
setDockBadge: (value: number | string, bounce?: boolean) => void,
): void {
ipcMain.on('notification', () => { ipcMain.on('notification', () => {
if (!isOSX() || mainWindow.isFocused()) { log.debug('ipcMain.notification');
if (!isOSX() || window.isFocused()) {
return; return;
} }
setDockBadge('•', options.bounce); setDockBadge('•', options.bounce);
}); });
mainWindow.on('focus', () => { window.on('focus', () => {
log.debug('mainWindow.focus');
setDockBadge(''); setDockBadge('');
}); });
} }
ipcMain.on('notification-click', () => { function setupSessionInteraction(window: BrowserWindow): void {
mainWindow.show(); // See API.md / "Accessing The Electron Session"
}); ipcMain.on(
'session-interaction',
(event, request: SessionInteractionRequest) => {
log.debug('ipcMain.session-interaction', { event, request });
mainWindow.webContents.on('new-window', onNewWindow); const result: SessionInteractionResult = { id: request.id };
mainWindow.webContents.on('will-navigate', onWillNavigate); let awaitingPromise = false;
mainWindow.webContents.on('did-finish-load', () => { try {
// Restore pinch-to-zoom, disabled by default in recent Electron. if (request.func !== undefined) {
// See https://github.com/jiahaog/nativefier/issues/379#issuecomment-598309817 // If no funcArgs provided, we'll just use an empty array
// and https://github.com/electron/electron/pull/12679 if (request.funcArgs === undefined || request.funcArgs === null) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises request.funcArgs = [];
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) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
clearCache(mainWindow);
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises // If funcArgs isn't an array, we'll be nice and make it a single item array
mainWindow.loadURL(options.targetUrl); if (typeof request.funcArgs[Symbol.iterator] !== 'function') {
request.funcArgs = [request.funcArgs];
// @ts-ignore
mainWindow.on('new-tab', () => createNewTab(options.targetUrl, true));
mainWindow.on('close', (event) => {
if (mainWindow.isFullScreen()) {
if (nativeTabsSupported()) {
mainWindow.moveTabToNewWindow();
} }
mainWindow.setFullScreen(false);
mainWindow.once( // Call func with funcArgs
'leave-full-screen', // @ts-expect-error accessing a func by string name
hideWindow.bind(this, mainWindow, event, options.fastQuit), // eslint-disable-next-line @typescript-eslint/no-unsafe-call
result.value = window.webContents.session[request.func](
...request.funcArgs,
);
if (result.value !== undefined && result.value instanceof Promise) {
// This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply
(result.value as Promise<unknown>)
.then((trueResultValue) => {
result.value = trueResultValue;
log.debug('ipcMain.session-interaction:result', result);
event.reply('session-interaction-reply', result);
})
.catch((err) =>
log.error('session-interaction ERROR', request, err),
);
awaitingPromise = true;
}
} else if (request.property !== undefined) {
if (request.propertyValue !== undefined) {
// Set the property
// @ts-expect-error setting a property by string name
window.webContents.session[request.property] =
request.propertyValue;
}
// Get the property value
// @ts-expect-error accessing a property by string name
result.value = window.webContents.session[request.property];
} else {
// Why even send the event if you're going to do this? You're just wasting time! ;)
throw new Error(
'Received neither a func nor a property in the request. Unable to process.',
); );
} }
hideWindow(mainWindow, event, options.fastQuit, options.tray);
if (options.clearCache) { // If we are awaiting a promise, that will return the reply instead, else
// eslint-disable-next-line @typescript-eslint/no-floating-promises if (!awaitingPromise) {
clearCache(mainWindow); log.debug('session-interaction:result', result);
event.reply('session-interaction-reply', result);
} }
}); } catch (err: unknown) {
log.error('session-interaction:error', err, event, request);
return mainWindow; result.error = err as Error;
result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place
event.reply('session-interaction-reply', result);
}
},
);
} }

View File

@ -1,238 +0,0 @@
import { onNewWindowHelper } from './mainWindowHelpers';
const originalUrl = 'https://medium.com/';
const internalUrl = 'https://medium.com/topics/technology';
const externalUrl = 'https://www.wikipedia.org/wiki/Electron';
const foregroundDisposition = 'foreground-tab';
const backgroundDisposition = 'background-tab';
const blockExternal = false;
const nativeTabsSupported = () => true;
const nativeTabsNotSupported = () => false;
test('internal urls should not be handled', () => {
const preventDefault = jest.fn();
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper(
internalUrl,
undefined,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsNotSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(0);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
});
test('external urls should be opened externally', () => {
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper(
externalUrl,
undefined,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsNotSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(1);
expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
});
test('external urls should be ignored if blockExternal is true', () => {
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
const blockExternal = true;
onNewWindowHelper(
externalUrl,
undefined,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsNotSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(1);
});
test('tab disposition should be ignored if tabs are not enabled', () => {
const preventDefault = jest.fn();
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper(
internalUrl,
foregroundDisposition,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsNotSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(0);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
});
test('tab disposition should be ignored if url is external', () => {
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper(
externalUrl,
foregroundDisposition,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(1);
expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
});
test('foreground tabs with internal urls should be opened in the foreground', () => {
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper(
internalUrl,
foregroundDisposition,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(1);
expect(createNewTab.mock.calls[0][1]).toBe(true);
expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
});
test('background tabs with internal urls should be opened in background tabs', () => {
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const preventDefault = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper(
internalUrl,
backgroundDisposition,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(0);
expect(createNewTab.mock.calls.length).toBe(1);
expect(createNewTab.mock.calls[0][1]).toBe(false);
expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
});
test('about:blank urls should be handled', () => {
const preventDefault = jest.fn();
const openExternal = jest.fn();
const createAboutBlankWindow = jest.fn();
const createNewTab = jest.fn();
const onBlockedExternalUrl = jest.fn();
onNewWindowHelper(
'about:blank',
undefined,
originalUrl,
undefined,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsNotSupported,
createNewTab,
blockExternal,
onBlockedExternalUrl,
);
expect(openExternal.mock.calls.length).toBe(0);
expect(createAboutBlankWindow.mock.calls.length).toBe(1);
expect(createNewTab.mock.calls.length).toBe(0);
expect(preventDefault.mock.calls.length).toBe(1);
expect(onBlockedExternalUrl.mock.calls.length).toBe(0);
});

View File

@ -1,35 +0,0 @@
import { linkIsInternal } from '../helpers/helpers';
export function onNewWindowHelper(
urlToGo: string,
disposition,
targetUrl: string,
internalUrls,
preventDefault,
openExternal,
createAboutBlankWindow,
nativeTabsSupported,
createNewTab,
blockExternal: boolean,
onBlockedExternalUrl: (url: string) => void,
): void {
if (!linkIsInternal(targetUrl, urlToGo, internalUrls)) {
preventDefault();
if (blockExternal) {
onBlockedExternalUrl(urlToGo);
} else {
openExternal(urlToGo);
}
} else if (urlToGo === 'about:blank') {
const newWindow = createAboutBlankWindow();
preventDefault(newWindow);
} else if (nativeTabsSupported()) {
if (disposition === 'background-tab') {
const newTab = createNewTab(urlToGo, false);
preventDefault(newTab);
} else if (disposition === 'foreground-tab') {
const newTab = createNewTab(urlToGo, true);
preventDefault(newTab);
}
}
}

View File

@ -0,0 +1,167 @@
import { BrowserWindow, MenuItemConstructorOptions } from 'electron';
jest.mock('../helpers/helpers');
import { isOSX } from '../helpers/helpers';
import { generateMenu } from './menu';
describe('generateMenu', () => {
let window: BrowserWindow;
const mockIsOSX: jest.SpyInstance = isOSX as jest.Mock;
let mockIsFullScreen: jest.SpyInstance;
let mockIsFullScreenable: jest.SpyInstance;
let mockIsSimpleFullScreen: jest.SpyInstance;
let mockSetFullScreen: jest.SpyInstance;
let mockSetSimpleFullScreen: jest.SpyInstance;
beforeEach(() => {
window = new BrowserWindow();
mockIsOSX.mockReset();
mockIsFullScreen = jest
.spyOn(window, 'isFullScreen')
.mockReturnValue(false);
mockIsFullScreenable = jest
.spyOn(window, 'isFullScreenable')
.mockReturnValue(true);
mockIsSimpleFullScreen = jest
.spyOn(window, 'isSimpleFullScreen')
.mockReturnValue(false);
mockSetFullScreen = jest.spyOn(window, 'setFullScreen');
mockSetSimpleFullScreen = jest.spyOn(window, 'setSimpleFullScreen');
});
afterAll(() => {
mockIsFullScreen.mockRestore();
mockIsFullScreenable.mockRestore();
mockIsSimpleFullScreen.mockRestore();
mockSetFullScreen.mockRestore();
mockSetSimpleFullScreen.mockRestore();
});
test('does not have fullscreen if not supported', () => {
mockIsOSX.mockReturnValue(false);
mockIsFullScreenable.mockReturnValue(false);
const menu = generateMenu(
{
nativefierVersion: '1.0.0',
zoom: 1.0,
disableDevTools: false,
},
window,
);
const editMenu = menu.filter((item) => item.label === '&View');
const fullscreen = (
editMenu[0].submenu as MenuItemConstructorOptions[]
).filter((item) => item.label === 'Toggle Full Screen');
expect(fullscreen).toHaveLength(1);
expect(fullscreen[0].enabled).toBe(false);
expect(fullscreen[0].visible).toBe(false);
expect(mockIsOSX).toHaveBeenCalled();
expect(mockIsFullScreenable).toHaveBeenCalled();
});
test('has fullscreen no matter what on mac', () => {
mockIsOSX.mockReturnValue(true);
mockIsFullScreenable.mockReturnValue(false);
const menu = generateMenu(
{
nativefierVersion: '1.0.0',
zoom: 1.0,
disableDevTools: false,
},
window,
);
const editMenu = menu.filter((item) => item.label === '&View');
const fullscreen = (
editMenu[0].submenu as MenuItemConstructorOptions[]
).filter((item) => item.label === 'Toggle Full Screen');
expect(fullscreen).toHaveLength(1);
expect(fullscreen[0].enabled).toBe(true);
expect(fullscreen[0].visible).toBe(true);
expect(mockIsOSX).toHaveBeenCalled();
expect(mockIsFullScreenable).toHaveBeenCalled();
});
test.each([true, false])(
'has a fullscreen menu item that toggles fullscreen',
(isFullScreen) => {
mockIsOSX.mockReturnValue(false);
mockIsFullScreenable.mockReturnValue(true);
mockIsFullScreen.mockReturnValue(isFullScreen);
const menu = generateMenu(
{
nativefierVersion: '1.0.0',
zoom: 1.0,
disableDevTools: false,
},
window,
);
const editMenu = menu.filter((item) => item.label === '&View');
const fullscreen = (
editMenu[0].submenu as MenuItemConstructorOptions[]
).filter((item) => item.label === 'Toggle Full Screen');
expect(fullscreen).toHaveLength(1);
expect(fullscreen[0].enabled).toBe(true);
expect(fullscreen[0].visible).toBe(true);
expect(mockIsOSX).toHaveBeenCalled();
expect(mockIsFullScreenable).toHaveBeenCalled();
// @ts-expect-error click is here TypeScript...
fullscreen[0].click(null, window);
expect(mockSetFullScreen).toHaveBeenCalledWith(!isFullScreen);
expect(mockSetSimpleFullScreen).not.toHaveBeenCalled();
},
);
test.each([true, false])(
'has a fullscreen menu item that toggles simplefullscreen as a fallback on mac',
(isFullScreen) => {
mockIsOSX.mockReturnValue(true);
mockIsFullScreenable.mockReturnValue(false);
mockIsSimpleFullScreen.mockReturnValue(isFullScreen);
const menu = generateMenu(
{
nativefierVersion: '1.0.0',
zoom: 1.0,
disableDevTools: false,
},
window,
);
const editMenu = menu.filter((item) => item.label === '&View');
const fullscreen = (
editMenu[0].submenu as MenuItemConstructorOptions[]
).filter((item) => item.label === 'Toggle Full Screen');
expect(fullscreen).toHaveLength(1);
expect(fullscreen[0].enabled).toBe(true);
expect(fullscreen[0].visible).toBe(true);
expect(mockIsOSX).toHaveBeenCalled();
expect(mockIsFullScreenable).toHaveBeenCalled();
// @ts-expect-error click is here TypeScript...
fullscreen[0].click(null, window);
expect(mockSetSimpleFullScreen).toHaveBeenCalledWith(!isFullScreen);
expect(mockSetFullScreen).not.toHaveBeenCalled();
},
);
});

View File

@ -1,22 +1,72 @@
import { Menu, clipboard, shell, MenuItemConstructorOptions } from 'electron'; import * as fs from 'fs';
import path from 'path';
export function createMenu({ import {
nativefierVersion, BrowserWindow,
appQuit, clipboard,
Menu,
MenuItem,
MenuItemConstructorOptions,
} from 'electron';
import { cleanupPlainText, isOSX, openExternal } from '../helpers/helpers';
import * as log from '../helpers/loggingHelper';
import {
clearAppData,
getCurrentURL,
goBack,
goForward,
goToURL,
zoomIn, zoomIn,
zoomOut, zoomOut,
zoomReset, zoomReset,
zoomBuildTimeValue, } from '../helpers/windowHelpers';
goBack, import { OutputOptions } from '../../../shared/src/options/model';
goForward,
getCurrentUrl, type BookmarksLink = {
clearAppData, type: 'link';
disableDevTools, title: string;
}): void { url: string;
shortcut?: string;
};
type BookmarksSeparator = {
type: 'separator';
};
type BookmarkConfig = BookmarksLink | BookmarksSeparator;
type BookmarksMenuConfig = {
menuLabel: string;
bookmarks: BookmarkConfig[];
};
export function createMenu(
options: OutputOptions,
mainWindow: BrowserWindow,
): void {
log.debug('createMenu', { options });
const menuTemplate = generateMenu(options, mainWindow);
injectBookmarks(menuTemplate);
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
}
export function generateMenu(
options: {
disableDevTools: boolean;
nativefierVersion: string;
zoom?: number;
},
mainWindow: BrowserWindow,
): MenuItemConstructorOptions[] {
const { nativefierVersion, zoom, disableDevTools } = options;
const zoomResetLabel = const zoomResetLabel =
zoomBuildTimeValue === 1.0 !zoom || zoom === 1.0
? 'Reset Zoom' ? 'Reset Zoom'
: `Reset Zoom (to ${zoomBuildTimeValue * 100}%, set at build time)`; : `Reset Zoom (to ${(zoom * 100).toFixed(1)}%, set at build time)`;
const editMenu: MenuItemConstructorOptions = { const editMenu: MenuItemConstructorOptions = {
label: '&Edit', label: '&Edit',
@ -44,13 +94,19 @@ export function createMenu({
accelerator: 'CmdOrCtrl+C', accelerator: 'CmdOrCtrl+C',
role: 'copy', role: 'copy',
}, },
{
label: 'Copy as Plain Text',
accelerator: 'CmdOrCtrl+Shift+C',
click: (): void => {
// We use clipboard.readText to strip down formatting
const text = clipboard.readText('selection');
clipboard.writeText(cleanupPlainText(text), 'clipboard');
},
},
{ {
label: 'Copy Current URL', label: 'Copy Current URL',
accelerator: 'CmdOrCtrl+L', accelerator: 'CmdOrCtrl+L',
click: () => { click: (): void => clipboard.writeText(getCurrentURL()),
const currentURL = getCurrentUrl();
clipboard.writeText(currentURL);
},
}, },
{ {
label: 'Paste', label: 'Paste',
@ -59,7 +115,10 @@ export function createMenu({
}, },
{ {
label: 'Paste and Match Style', label: 'Paste and Match Style',
accelerator: 'CmdOrCtrl+Shift+V', // https://github.com/nativefier/nativefier/issues/404
// Apple's HIG lists this shortcut for paste and match style
// https://support.apple.com/en-us/HT209651
accelerator: isOSX() ? 'Option+Shift+Cmd+V' : 'Ctrl+Shift+V',
role: 'pasteAndMatchStyle', role: 'pasteAndMatchStyle',
}, },
{ {
@ -69,7 +128,22 @@ export function createMenu({
}, },
{ {
label: 'Clear App Data', label: 'Clear App Data',
click: clearAppData, click: (
item: MenuItem,
focusedWindow: BrowserWindow | undefined,
): void => {
log.debug('Clear App Data.click', {
item,
focusedWindow,
mainWindow,
});
if (!focusedWindow) {
focusedWindow = mainWindow;
}
clearAppData(focusedWindow).catch((err) =>
log.error('clearAppData ERROR', err),
);
},
}, },
], ],
}; };
@ -79,11 +153,7 @@ export function createMenu({
submenu: [ submenu: [
{ {
label: 'Back', label: 'Back',
accelerator: (() => { accelerator: isOSX() ? 'Cmd+Left' : 'Alt+Left',
const backKbShortcut =
process.platform === 'darwin' ? 'Cmd+Left' : 'Alt+Left';
return backKbShortcut;
})(),
click: goBack, click: goBack,
}, },
{ {
@ -95,11 +165,7 @@ export function createMenu({
}, },
{ {
label: 'Forward', label: 'Forward',
accelerator: (() => { accelerator: isOSX() ? 'Cmd+Right' : 'Alt+Right',
const forwardKbShortcut =
process.platform === 'darwin' ? 'Cmd+Right' : 'Alt+Right';
return forwardKbShortcut;
})(),
click: goForward, click: goForward,
}, },
{ {
@ -111,27 +177,35 @@ export function createMenu({
}, },
{ {
label: 'Reload', label: 'Reload',
accelerator: 'CmdOrCtrl+R', role: 'reload',
click: (item, focusedWindow) => {
if (focusedWindow) {
focusedWindow.reload();
}
},
}, },
{ {
type: 'separator', type: 'separator',
}, },
{ {
label: 'Toggle Full Screen', label: 'Toggle Full Screen',
accelerator: (() => { accelerator: isOSX() ? 'Ctrl+Cmd+F' : 'F11',
if (process.platform === 'darwin') { enabled: mainWindow.isFullScreenable() || isOSX(),
return 'Ctrl+Cmd+F'; visible: mainWindow.isFullScreenable() || isOSX(),
click: (
item: MenuItem,
focusedWindow: BrowserWindow | undefined,
): void => {
log.debug('Toggle Full Screen.click()', {
item,
focusedWindow,
isFullScreen: focusedWindow?.isFullScreen(),
isFullScreenable: focusedWindow?.isFullScreenable(),
});
if (!focusedWindow) {
focusedWindow = mainWindow;
} }
return 'F11'; if (focusedWindow.isFullScreenable()) {
})(),
click: (item, focusedWindow) => {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
} else if (isOSX()) {
focusedWindow.setSimpleFullScreen(
!focusedWindow.isSimpleFullScreen(),
);
} }
}, },
}, },
@ -162,14 +236,14 @@ export function createMenu({
{ {
label: zoomResetLabel, label: zoomResetLabel,
accelerator: 'CmdOrCtrl+0', accelerator: 'CmdOrCtrl+0',
click: zoomReset, click: (): void => zoomReset(options),
}, },
{ {
label: 'ZoomResetAdditionalShortcut', label: 'ZoomResetAdditionalShortcut',
visible: false, visible: false,
acceleratorWorksWhenHidden: true, acceleratorWorksWhenHidden: true,
accelerator: 'CmdOrCtrl+num0', accelerator: 'CmdOrCtrl+num0',
click: zoomReset, click: (): void => zoomReset(options),
}, },
], ],
}; };
@ -181,16 +255,13 @@ export function createMenu({
}, },
{ {
label: 'Toggle Developer Tools', label: 'Toggle Developer Tools',
accelerator: (() => { accelerator: isOSX() ? 'Alt+Cmd+I' : 'Ctrl+Shift+I',
if (process.platform === 'darwin') { click: (item: MenuItem, focusedWindow: BrowserWindow | undefined) => {
return 'Alt+Cmd+I'; log.debug('Toggle Developer Tools.click()', { item, focusedWindow });
if (!focusedWindow) {
focusedWindow = mainWindow;
} }
return 'Ctrl+Shift+I';
})(),
click: (item, focusedWindow) => {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools(); focusedWindow.webContents.toggleDevTools();
}
}, },
}, },
); );
@ -219,16 +290,23 @@ export function createMenu({
submenu: [ submenu: [
{ {
label: `Built with Nativefier v${nativefierVersion}`, label: `Built with Nativefier v${nativefierVersion}`,
click: () => { click: (): void => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises openExternal('https://github.com/nativefier/nativefier').catch(
shell.openExternal('https://github.com/jiahaog/nativefier'); (err: unknown): void =>
log.error(
'Built with Nativefier v${nativefierVersion}.click ERROR',
err,
),
);
}, },
}, },
{ {
label: 'Report an Issue', label: 'Report an Issue',
click: () => { click: (): void => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises openExternal('https://github.com/nativefier/nativefier/issues').catch(
shell.openExternal('https://github.com/jiahaog/nativefier/issues'); (err: unknown): void =>
log.error('Report an Issue.click ERROR', err),
);
}, },
}, },
], ],
@ -236,7 +314,7 @@ export function createMenu({
let menuTemplate: MenuItemConstructorOptions[]; let menuTemplate: MenuItemConstructorOptions[];
if (process.platform === 'darwin') { if (isOSX()) {
const electronMenu: MenuItemConstructorOptions = { const electronMenu: MenuItemConstructorOptions = {
label: 'E&lectron', label: 'E&lectron',
submenu: [ submenu: [
@ -268,7 +346,7 @@ export function createMenu({
{ {
label: 'Quit', label: 'Quit',
accelerator: 'Cmd+Q', accelerator: 'Cmd+Q',
click: appQuit, role: 'quit',
}, },
], ],
}; };
@ -286,6 +364,61 @@ export function createMenu({
menuTemplate = [editMenu, viewMenu, windowMenu, helpMenu]; menuTemplate = [editMenu, viewMenu, windowMenu, helpMenu];
} }
const menu = Menu.buildFromTemplate(menuTemplate); return menuTemplate;
Menu.setApplicationMenu(menu); }
function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void {
const bookmarkConfigPath = path.join(__dirname, '..', 'bookmarks.json');
if (!fs.existsSync(bookmarkConfigPath)) {
return;
}
try {
const bookmarksMenuConfig = JSON.parse(
fs.readFileSync(bookmarkConfigPath, 'utf-8'),
) as BookmarksMenuConfig;
const submenu: MenuItemConstructorOptions[] =
bookmarksMenuConfig.bookmarks.map((bookmark) => {
switch (bookmark.type) {
case 'link':
if (!('title' in bookmark && 'url' in bookmark)) {
throw new Error(
'All links in the bookmarks menu must have a title and url.',
);
}
try {
new URL(bookmark.url);
} catch {
throw new Error('Bookmark URL "' + bookmark.url + '"is invalid.');
}
return {
label: bookmark.title,
click: (): void => {
goToURL(bookmark.url)?.catch((err: unknown): void =>
log.error(`${bookmark.title}.click ERROR`, err),
);
},
accelerator:
'shortcut' in bookmark ? bookmark.shortcut : undefined,
};
case 'separator':
return {
type: 'separator',
};
default:
throw new Error(
'A bookmarks menu entry has an invalid type; type must be one of "link", "separator".',
);
}
});
const bookmarksMenu: MenuItemConstructorOptions = {
label: bookmarksMenuConfig.menuLabel,
submenu,
};
// Insert custom bookmarks menu between menus "View" and "Window"
menuTemplate.splice(menuTemplate.length - 2, 0, bookmarksMenu);
} catch (err: unknown) {
log.error('Failed to load & parse bookmarks configuration JSON file.', err);
}
} }

View File

@ -1,19 +1,34 @@
import { app, Tray, Menu, ipcMain, nativeImage, BrowserWindow } from 'electron'; import { app, Tray, Menu, ipcMain, nativeImage, BrowserWindow } from 'electron';
import { getAppIcon, getCounterValue } from '../helpers/helpers'; import { getAppIcon, getCounterValue, isOSX } from '../helpers/helpers';
import * as log from '../helpers/loggingHelper';
import { OutputOptions } from '../../../shared/src/options/model';
export function createTrayIcon( export function createTrayIcon(
nativefierOptions, nativefierOptions: OutputOptions,
mainWindow: BrowserWindow, mainWindow: BrowserWindow,
): Tray { ): Tray | undefined {
const options = { ...nativefierOptions }; const options = { ...nativefierOptions };
if (options.tray) { if (options.tray && options.tray !== 'false') {
const iconPath = getAppIcon(); const iconPath = getAppIcon();
if (!iconPath) {
throw new Error('Icon path not found found to use with tray option.');
}
const nimage = nativeImage.createFromPath(iconPath); const nimage = nativeImage.createFromPath(iconPath);
const appIcon = new Tray(nimage); const appIcon = new Tray(nativeImage.createEmpty());
const onClick = () => { if (isOSX()) {
//sets the icon to the height of the tray.
appIcon.setImage(
nimage.resize({ height: appIcon.getBounds().height - 2 }),
);
} else {
appIcon.setImage(nimage);
}
const onClick = (): void => {
log.debug('onClick');
if (mainWindow.isVisible()) { if (mainWindow.isVisible()) {
mainWindow.hide(); mainWindow.hide();
} else { } else {
@ -28,39 +43,46 @@ export function createTrayIcon(
}, },
{ {
label: 'Quit', label: 'Quit',
click: app.exit.bind(this), click: (): void => app.exit(0),
}, },
]); ]);
appIcon.on('click', onClick); appIcon.on('click', onClick);
if (options.counter) { if (options.counter) {
mainWindow.on('page-title-updated', (e, title) => { mainWindow.on('page-title-updated', (event, title) => {
log.debug('mainWindow.page-title-updated', { event, title });
const counterValue = getCounterValue(title); const counterValue = getCounterValue(title);
if (counterValue) { if (counterValue) {
appIcon.setToolTip(`(${counterValue}) ${options.name}`); appIcon.setToolTip(
`(${counterValue}) ${options.name ?? 'Nativefier'}`,
);
} else { } else {
appIcon.setToolTip(options.name); appIcon.setToolTip(options.name ?? '');
} }
}); });
} else { } else {
ipcMain.on('notification', () => { ipcMain.on('notification', () => {
log.debug('ipcMain.notification');
if (mainWindow.isFocused()) { if (mainWindow.isFocused()) {
return; return;
} }
if (options.name) {
appIcon.setToolTip(`${options.name}`); appIcon.setToolTip(`${options.name}`);
}
}); });
mainWindow.on('focus', () => { mainWindow.on('focus', () => {
appIcon.setToolTip(options.name); log.debug('mainWindow.focus');
appIcon.setToolTip(options.name ?? '');
}); });
} }
appIcon.setToolTip(options.name); appIcon.setToolTip(options.name ?? '');
appIcon.setContextMenu(contextMenu); appIcon.setContextMenu(contextMenu);
return appIcon; return appIcon;
} }
return null; return undefined;
} }

View File

@ -1,34 +1,246 @@
import { linkIsInternal, getCounterValue } from './helpers'; import { shell } from 'electron';
jest.mock('./windowHelpers');
import {
cleanupPlainText,
getCounterValue,
linkIsInternal,
openExternal,
removeUserAgentSpecifics,
} from './helpers';
import { showNavigationBlockedMessage } from './windowHelpers';
const internalUrl = 'https://medium.com/'; const internalUrl = 'https://medium.com/';
const internalUrlWww = 'https://www.medium.com/';
const internalUrlSubPathRegex = /https:\/\/www.medium.com\/.*/;
const sameBaseDomainUrl = 'https://app.medium.com/';
const internalUrlCoUk = 'https://medium.co.uk/';
const differentBaseDomainUrlCoUk = 'https://other.domain.co.uk/';
const sameBaseDomainUrlCoUk = 'https://app.medium.co.uk/';
const sameBaseDomainUrlTidalListen = 'https://listen.tidal.com/';
const sameBaseDomainUrlTidalLogin = 'https://login.tidal.com/';
const sameBaseDomainUrlTidalRegex = /https:\/\/(login|listen).tidal.com\/.*/;
const internalUrlSubPath = 'topic/technology'; const internalUrlSubPath = 'topic/technology';
const externalUrl = 'https://www.wikipedia.org/wiki/Electron'; const externalUrl = 'https://www.wikipedia.org/wiki/Electron';
const wildcardRegex = /.*/; const wildcardRegex = /.*/;
test('the original url should be internal', () => { test('the original url should be internal without --strict-internal-urls', () => {
expect(linkIsInternal(internalUrl, internalUrl, undefined)).toEqual(true);
});
test('sub-paths of the original url should be internal', () => {
expect( expect(
linkIsInternal(internalUrl, internalUrl + internalUrlSubPath, undefined), linkIsInternal(internalUrl, internalUrl, undefined, undefined),
).toEqual(true); ).toEqual(true);
}); });
test("'about:blank' should be internal", () => { test('the original url should be internal with --strict-internal-urls off', () => {
expect(linkIsInternal(internalUrl, 'about:blank', undefined)).toEqual(true); expect(linkIsInternal(internalUrl, internalUrl, undefined, false)).toEqual(
true,
);
});
test('the original url should be internal with --strict-internal-urls on', () => {
expect(linkIsInternal(internalUrl, internalUrl, undefined, true)).toEqual(
true,
);
});
test('sub-paths of the original url should be internal with --strict-internal-urls off', () => {
expect(
linkIsInternal(
internalUrl,
internalUrl + internalUrlSubPath,
undefined,
false,
),
).toEqual(true);
});
test('sub-paths of the original url should not be internal with --strict-internal-urls on', () => {
expect(
linkIsInternal(
internalUrl,
internalUrl + internalUrlSubPath,
undefined,
true,
),
).toEqual(false);
});
test('sub-paths of the original url should be internal with using a regex and --strict-internal-urls on', () => {
expect(
linkIsInternal(
internalUrl,
internalUrl + internalUrlSubPath,
internalUrlSubPathRegex,
true,
),
).toEqual(false);
});
test("'about:blank' should always be internal", () => {
expect(linkIsInternal(internalUrl, 'about:blank', undefined, true)).toEqual(
true,
);
}); });
test('urls from different sites should not be internal', () => { test('urls from different sites should not be internal', () => {
expect(linkIsInternal(internalUrl, externalUrl, undefined)).toEqual(false); expect(linkIsInternal(internalUrl, externalUrl, undefined, false)).toEqual(
false,
);
}); });
test('all urls should be internal with wildcard regex', () => { test('all urls should be internal with wildcard regex', () => {
expect(linkIsInternal(internalUrl, externalUrl, wildcardRegex)).toEqual(true); expect(linkIsInternal(internalUrl, externalUrl, wildcardRegex, true)).toEqual(
true,
);
}); });
test('a "www." of a domain should be considered internal', () => {
expect(linkIsInternal(internalUrl, internalUrlWww, undefined, false)).toEqual(
true,
);
});
test('urls on the same "base domain" should be considered internal', () => {
expect(
linkIsInternal(internalUrl, sameBaseDomainUrl, undefined, false),
).toEqual(true);
});
test('urls on the same "base domain" should NOT be considered internal using --strict-internal-urls', () => {
expect(
linkIsInternal(internalUrl, sameBaseDomainUrl, undefined, true),
).toEqual(false);
});
test('urls on the same "base domain" should be considered internal, even with a www', () => {
expect(
linkIsInternal(internalUrlWww, sameBaseDomainUrl, undefined, false),
).toEqual(true);
});
test('urls on the same "base domain" should be considered internal, even with different sub domains', () => {
expect(
linkIsInternal(
sameBaseDomainUrlTidalListen,
sameBaseDomainUrlTidalLogin,
undefined,
false,
),
).toEqual(true);
});
test('urls should support sub domain matching with a regex', () => {
expect(
linkIsInternal(
sameBaseDomainUrlTidalListen,
sameBaseDomainUrlTidalLogin,
sameBaseDomainUrlTidalRegex,
false,
),
).toEqual(true);
});
test('urls on the same "base domain" should NOT be considered internal with different sub domains when using --strict-internal-urls', () => {
expect(
linkIsInternal(
sameBaseDomainUrlTidalListen,
sameBaseDomainUrlTidalLogin,
undefined,
true,
),
).toEqual(false);
});
test('urls on the same "base domain" should be considered internal, long SLD', () => {
expect(
linkIsInternal(internalUrlCoUk, sameBaseDomainUrlCoUk, undefined, false),
).toEqual(true);
});
test('urls on the a different "base domain" are considered NOT internal, long SLD', () => {
expect(
linkIsInternal(
internalUrlCoUk,
differentBaseDomainUrlCoUk,
undefined,
false,
),
).toEqual(false);
});
const testLoginPages = [
'https://amazon.co.uk/signin',
'https://amazon.com/signin',
'https://amazon.de/signin',
'https://amazon.com/ap/signin',
'https://facebook.co.uk/login',
'https://facebook.com/login',
'https://facebook.de/login',
'https://github.co.uk/login',
'https://github.com/login',
'https://github.de/login',
// GitHub 2FA flow with FIDO token
'https://github.com/session',
'https://github.com/sessions/two-factor/webauth',
'https://accounts.google.co.uk',
'https://accounts.google.com',
'https://mail.google.com/accounts/SetOSID',
'https://mail.google.co.uk/accounts/SetOSID',
'https://accounts.google.de',
'https://linkedin.co.uk/uas/login',
'https://linkedin.com/uas/login',
'https://linkedin.de/uas/login',
'https://login.live.co.uk',
'https://login.live.com',
'https://login.live.de',
'https://login.microsoftonline.com/common/oauth2/authorize',
'https://login.microsoftonline.co.uk/common/oauth2/authorize',
'https://login.microsoftonline.de/common/oauth2/authorize',
'https://okta.co.uk',
'https://okta.com',
'https://subdomain.okta.com',
'https://okta.de',
'https://twitter.co.uk/oauth/authenticate',
'https://twitter.com/oauth/authenticate',
'https://twitter.de/oauth/authenticate',
'https://appleid.apple.com/auth/authorize',
'https://id.atlassian.com',
'https://auth.atlassian.com',
'https://vmware.workspaceair.com',
'https://vmware.auth.securid.com',
];
test.each(testLoginPages)(
'%s login page should be internal',
(loginUrl: string) => {
expect(linkIsInternal(internalUrl, loginUrl, undefined, false)).toEqual(
true,
);
},
);
// Ensure that we don't over-match service pages
const testNonLoginPages = [
'https://www.amazon.com/Node-Cookbook-techniques-server-side-development-ebook',
'https://github.com/nativefier/nativefier',
'https://github.com/org/nativefier',
'https://microsoft.com',
'https://office.microsoftonline.com',
'https://twitter.com/marcoroth_/status/1325938620906287104',
'https://appleid.apple.com/account',
'https://mail.google.com/',
'https://atlassian.com',
];
test.each(testNonLoginPages)(
'%s page should not be internal',
(url: string) => {
expect(linkIsInternal(internalUrl, url, undefined, false)).toEqual(false);
},
);
const smallCounterTitle = 'Inbox (11) - nobody@example.com - Gmail'; const smallCounterTitle = 'Inbox (11) - nobody@example.com - Gmail';
const largeCounterTitle = 'Inbox (8,756) - nobody@example.com - Gmail'; const largeCounterTitle = 'Inbox (8,756) - nobody@example.com - Gmail';
const hourCounterTitle = 'Today (1:23) - nobody@example.com - TimeTracker';
const noCounterTitle = 'Inbox - nobody@example.com - Gmail'; const noCounterTitle = 'Inbox - nobody@example.com - Gmail';
test('getCounterValue should return undefined for titles without counter numbers', () => { test('getCounterValue should return undefined for titles without counter numbers', () => {
@ -42,3 +254,95 @@ test('getCounterValue should return a string for small counter numbers in the ti
test('getCounterValue should return a string for large counter numbers in the title', () => { test('getCounterValue should return a string for large counter numbers in the title', () => {
expect(getCounterValue(largeCounterTitle)).toEqual('8,756'); expect(getCounterValue(largeCounterTitle)).toEqual('8,756');
}); });
test('getCounterValue should return a string for hour counter numbers in the title', () => {
expect(getCounterValue(hourCounterTitle)).toEqual('1:23');
});
describe('removeUserAgentSpecifics', () => {
const userAgentFallback =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) app-nativefier-804458/1.0.0 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36';
test('removes Electron and App specific info', () => {
expect(
removeUserAgentSpecifics(
userAgentFallback,
'app-nativefier-804458',
'1.0.0',
),
).not.toBe(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36',
);
});
test('should not have multiple spaces in a row', () => {
expect(
removeUserAgentSpecifics(
userAgentFallback,
'app-nativefier-804458',
'1.0.0',
),
).toEqual(expect.not.stringMatching(/\s{2,}/));
});
});
describe('cleanupPlainText', () => {
test('removes extra spaces from text', () => {
expect(cleanupPlainText(' this is a test ')).toBe('this is a test');
});
});
describe('openExternal', () => {
const mockShellOpenExternal: jest.SpyInstance = jest.spyOn(
shell,
'openExternal',
);
const mockShowNavigationBlockedMessage: jest.SpyInstance =
showNavigationBlockedMessage as jest.Mock;
beforeEach(() => {
mockShellOpenExternal.mockReset();
mockShowNavigationBlockedMessage
.mockReset()
.mockReturnValue(Promise.resolve(undefined));
});
afterAll(() => {
mockShellOpenExternal.mockRestore();
mockShowNavigationBlockedMessage.mockRestore();
});
test('https urls scheme should *not* be blocked', async () => {
await openExternal('https://whatever.foo');
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockShellOpenExternal).toHaveBeenCalled();
});
test('urls with whitelisted scheme should *not* be blocked', async () => {
await openExternal('ircs://irc.libera.chat/whatever');
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockShellOpenExternal).toHaveBeenCalled();
});
test('urls with non-allowlisted scheme *should* be blocked', async () => {
await openExternal('barf://whatever.foo');
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockShellOpenExternal).not.toHaveBeenCalled();
});
test('potentially-malicious urls *should* be blocked', async () => {
await openExternal('https://hello.com/wor%00ld');
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockShellOpenExternal).not.toHaveBeenCalled();
});
test('malformed urls *should* be blocked', async () => {
await openExternal('zombocom');
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockShellOpenExternal).not.toHaveBeenCalled();
});
});

View File

@ -2,53 +2,103 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { BrowserWindow } from 'electron'; import { BrowserWindow, OpenExternalOptions, shell } from 'electron';
import wurl from 'wurl';
const INJECT_CSS_PATH = path.join(__dirname, '..', 'inject/inject.css'); import * as log from '../helpers/loggingHelper';
import { showNavigationBlockedMessage } from './windowHelpers';
export function isOSX(): boolean { export const INJECT_DIR = path.join(__dirname, '..', 'inject');
return os.platform() === 'darwin';
}
export function isLinux(): boolean { /**
return os.platform() === 'linux'; * Firefox's list of protocols for which opening an external handler is allowed without confirmation.
} * Taken from Firefox's. Location might vary in codebase, search for one of them, e.g.
* https://searchfox.org/mozilla-central/search?q=%22xmpp%22&path=&case=false&regexp=false
*/
const URL_PROTOCOLS_NOCONFIRMATION_FIREFOX = [
'bitcoin:',
'ftp:',
'ftps:',
'geo:',
'im:',
'irc:',
'ircs:',
'magnet:',
'mailto:',
'matrix:',
'mms:',
'news:',
'nntp:',
'openpgp4fpr:',
'sftp:',
'sip:',
'sms:',
'smsto:',
'ssh:',
'tel:',
'urn:',
'webcal:',
'wtai:',
'xmpp:',
];
/**
* Our extension to Firefox's list. If extending this list too much, we should
* really add a confirmation modal (for now we just block), like browsers do.
* But for now, since nobody shouts at us for bluntly blocking anything else,
* let's keep rolling with it.
*/
const URL_PROTOCOLS_NOCONFIRMATION_EXTRA = ['zoommtg:'];
/**
* List of protocols for which opening an external handler is allowed without confirmation.
* Note: "without confirmation" is currently a lie. It was implemented this way
* as a way to know from user feedback what protocols would cause users to shout,
* but there wasn't much shouting happening, so we currently don't have a confirmation
* mechanism, we just bluntly block. That might need to change at some point.
*/
const URL_PROTOCOLS_NOCONFIRMATION = [
'http:',
'https:',
...URL_PROTOCOLS_NOCONFIRMATION_FIREFOX,
...URL_PROTOCOLS_NOCONFIRMATION_EXTRA,
];
const SHELL_SAFETY_FEEDBACK_STR =
'If you believe this URL should open, you might be right, and our validation might be excessive.' +
'Please share this error & URL at https://github.com/nativefier/nativefier/issues/1459';
export function isWindows(): boolean { export function isUrlShellSafe(
return os.platform() === 'win32'; urlToGo: string,
} ): { blocked: false } | { blocked: true; reason: string } {
let url: URL;
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 { try {
fs.accessSync(INJECT_CSS_PATH); url = new URL(urlToGo.toLowerCase());
return true; } catch (err: unknown) {
} catch (e) { return {
return false; blocked: true,
} reason: `URL appears malformed. ${SHELL_SAFETY_FEEDBACK_STR}`,
};
} }
export function getCssToInject(): string { if (!URL_PROTOCOLS_NOCONFIRMATION.includes(url.protocol)) {
return fs.readFileSync(INJECT_CSS_PATH).toString(); return {
blocked: true,
reason: `URL protocol is disallowed. ${SHELL_SAFETY_FEEDBACK_STR}`,
};
}
// https://cwe.mitre.org/data/definitions/177.html
if (
urlToGo.includes('%00') ||
urlToGo.includes('%0a') ||
urlToGo.includes('%2e') ||
urlToGo.includes('%2f') ||
urlToGo.includes('%5c')
) {
return {
blocked: true,
reason: `URL might be malicious. ${SHELL_SAFETY_FEEDBACK_STR}`,
};
}
return { blocked: false };
} }
/** /**
@ -59,10 +109,31 @@ export function debugLog(browserWindow: BrowserWindow, message: string): void {
setTimeout(() => { setTimeout(() => {
browserWindow.webContents.send('debug', message); browserWindow.webContents.send('debug', message);
}, 3000); }, 3000);
console.info(message); log.debug(message);
} }
export function getAppIcon(): string { /**
* Helper to determine domain-ish equality for many cases, the trivial ones
* and the trickier ones, e.g. `blog.foo.com` and `shop.foo.com`,
* in a way that is "good enough", and doesn't need a list of SLDs.
* See chat at https://github.com/nativefier/nativefier/pull/1171#pullrequestreview-649132523
*/
function domainify(url: string): string {
// So here's what we're doing here:
// Get the hostname from the url
const hostname = new URL(url).hostname;
// Drop the first section if the domain
const domain = hostname.split('.').slice(1).join('.');
// Check the length, if it's too short, the hostname was probably the domain
// Or if the domain doesn't have a . in it we went too far
if (domain.length < 6 || domain.split('.').length === 0) {
return hostname;
}
// This SHOULD be the domain, but nothing is 100% guaranteed
return domain;
}
export function getAppIcon(): string | undefined {
// Prefer ICO under Windows, see // Prefer ICO under Windows, see
// https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions // https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions
// https://www.electronjs.org/docs/api/native-image#supported-formats // https://www.electronjs.org/docs/api/native-image#supported-formats
@ -78,12 +149,167 @@ export function getAppIcon(): string {
} }
} }
export function getCounterValue(title: string): string | undefined {
const itemCountRegex = /[([{]([\d.,:]*)\+?[}\])]/;
const match = itemCountRegex.exec(title);
return match ? match[1] : undefined;
}
export function getCSSToInject(): string {
let cssToInject = '';
const cssFiles = fs
.readdirSync(INJECT_DIR, { withFileTypes: true })
.filter(
(injectFile) => injectFile.isFile() && injectFile.name.endsWith('.css'),
)
.map((cssFileStat) =>
path.resolve(path.join(INJECT_DIR, cssFileStat.name)),
);
for (const cssFile of cssFiles) {
log.debug('Injecting CSS file', cssFile);
const cssFileData = fs.readFileSync(cssFile);
cssToInject += `/* ${cssFile} */\n\n ${cssFileData.toString()}\n\n`;
}
return cssToInject;
}
export function isOSX(): boolean {
return os.platform() === 'darwin';
}
export function isLinux(): boolean {
return os.platform() === 'linux';
}
export function isWindows(): boolean {
return os.platform() === 'win32';
}
function isInternalLoginPage(url: string): boolean {
// Making changes? Remember to update the tests in helpers.test.ts and in API.md
const internalLoginPagesArray = [
'amazon\\.[a-zA-Z\\.]*/[a-zA-Z\\/]*signin', // Amazon
`facebook\\.[a-zA-Z\\.]*\\/login`, // Facebook
'github\\.[a-zA-Z\\.]*\\/(?:login|session)', // GitHub
'accounts\\.google\\.[a-zA-Z\\.]*', // Google
'mail\\.google\\.[a-zA-Z\\.]*\\/accounts/SetOSID', // Google
'linkedin\\.[a-zA-Z\\.]*/uas/login', // LinkedIn
'login\\.live\\.[a-zA-Z\\.]*', // Microsoft
'login\\.microsoftonline\\.[a-zA-Z\\.]*', // Microsoft
'okta\\.[a-zA-Z\\.]*', // Okta
'twitter\\.[a-zA-Z\\.]*/oauth/authenticate', // Twitter
'appleid\\.apple\\.com/auth/authorize', // Apple
'(?:id|auth)\\.atlassian\\.[a-zA-Z]+', // Atlassian
'.*\\.workspaceair\\.com', // VMWare Workspace One SSO
'.*\\.securid\\.com', // SecurID for VMWare Workspace One SSO
];
// Making changes? Remember to update the tests in helpers.test.ts and in API.md
const regex = RegExp(internalLoginPagesArray.join('|'));
return regex.test(url);
}
export function linkIsInternal(
currentUrl: string,
newUrl: string,
internalUrlRegex: string | RegExp | undefined,
isStrictInternalUrlsEnabled: boolean | undefined,
): boolean {
log.debug('linkIsInternal', { currentUrl, newUrl, internalUrlRegex });
if (newUrl.split('#')[0] === 'about:blank') {
return true;
}
if (isInternalLoginPage(newUrl)) {
return true;
}
if (internalUrlRegex) {
const regex = RegExp(internalUrlRegex);
if (regex.test(newUrl)) {
return true;
}
}
if (isStrictInternalUrlsEnabled) {
return currentUrl == newUrl;
}
try {
// Consider as "same domain-ish", without TLD/SLD list:
// 1. app.foo.com and foo.com
// 2. www.foo.com and foo.com
// 3. www.foo.com and app.foo.com
// Only use the tld and the main domain for domain-ish test
// Enables domain-ish equality for blog.foo.com and shop.foo.com
return domainify(currentUrl) === domainify(newUrl);
} catch (err: unknown) {
log.error(
'Failed to parse domains as determining if link is internal. From:',
currentUrl,
'To:',
newUrl,
err,
);
return false;
}
}
export function nativeTabsSupported(): boolean { export function nativeTabsSupported(): boolean {
return isOSX(); return isOSX();
} }
export function getCounterValue(title: string): string { /**
const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/; * Open the given external protocol URL in the desktop's default manner
const match = itemCountRegex.exec(title); * (e.g. `mailto:` URLs in the user's default mail agent), with extra validation.
return match ? match[1] : undefined; */
export function openExternal(
url: string,
options?: OpenExternalOptions,
): Promise<void> {
const urlShellSafety = isUrlShellSafe(url);
log.debug('openExternal', { url, options, urlShellSafety });
if (urlShellSafety.blocked) {
return new Promise((resolve) => {
showNavigationBlockedMessage(
`Navigation blocked to ${url}\n\n${urlShellSafety.reason}`,
)
.then(() => resolve())
.catch((err: unknown) => {
throw err;
});
});
}
return shell.openExternal(url, options);
}
// Copy-pastaed as unable to get imports to work in preload.
// If modifying, update also app/src/preload.ts
export function isWayland(): boolean {
return (
isLinux() &&
(Boolean(process.env.WAYLAND_DISPLAY) ||
process.env.XDG_SESSION_TYPE === 'wayland')
);
}
export function removeUserAgentSpecifics(
userAgentFallback: string,
appName: string,
appVersion: string,
): string {
// Electron userAgentFallback is the user agent used if none is specified when creating a window.
// For our purposes, it's useful because its format is similar enough to a real Chrome's user agent to not need
// to infer the userAgent. userAgentFallback normally looks like this:
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) app-nativefier-804458/1.0.0 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36
// We just need to strip out the appName/1.0.0 and Electron/electronVersion
return userAgentFallback
.replace(`Electron/${process.versions.electron} `, '')
.replace(`${appName}/${appVersion} `, '');
}
/** Removes extra spaces from a text */
export function cleanupPlainText(text: string): string {
return text.trim().replace(/\s+/g, ' ');
} }

View File

@ -2,6 +2,9 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { isOSX, isWindows, isLinux } from './helpers'; import { isOSX, isWindows, isLinux } from './helpers';
import * as log from './loggingHelper';
type fsError = Error & { code: string };
/** /**
* Find a file or directory * Find a file or directory
@ -13,12 +16,12 @@ function findSync(
): string[] { ): string[] {
const matches: string[] = []; const matches: string[] = [];
(function findSyncRecurse(base) { (function findSyncRecurse(base): void {
let children: string[]; let children: string[];
try { try {
children = fs.readdirSync(base); children = fs.readdirSync(base);
} catch (err) { } catch (err: unknown) {
if (err.code === 'ENOENT') { if ((err as fsError).code === 'ENOENT') {
return; return;
} }
throw err; throw err;
@ -50,18 +53,18 @@ function findSync(
return matches; return matches;
} }
function findFlashOnLinux() { function findFlashOnLinux(): string {
return findSync(/libpepflashplayer\.so/, '/opt/google/chrome')[0]; return findSync(/libpepflashplayer\.so/, '/opt/google/chrome')[0];
} }
function findFlashOnWindows() { function findFlashOnWindows(): string {
return findSync( return findSync(
/pepflashplayer\.dll/, /pepflashplayer\.dll/,
'C:\\Program Files (x86)\\Google\\Chrome', 'C:\\Program Files (x86)\\Google\\Chrome',
)[0]; )[0];
} }
function findFlashOnMac() { function findFlashOnMac(): string {
return findSync( return findSync(
/PepperFlashPlayer.plugin/, /PepperFlashPlayer.plugin/,
'/Applications/Google Chrome.app/', '/Applications/Google Chrome.app/',
@ -69,7 +72,7 @@ function findFlashOnMac() {
)[0]; )[0];
} }
export function inferFlashPath() { export function inferFlashPath(): string | undefined {
if (isOSX()) { if (isOSX()) {
return findFlashOnMac(); return findFlashOnMac();
} }
@ -82,6 +85,6 @@ export function inferFlashPath() {
return findFlashOnLinux(); return findFlashOnLinux();
} }
console.warn('Unable to determine OS to infer flash player'); log.warn('Unable to determine OS to infer flash player');
return null; return undefined;
} }

View File

@ -0,0 +1,82 @@
// This helper allows logs to either be printed to the console as they would normally or if
// the USE_LOG_FILE environment variable is set (such as through our playwright tests), then
// the logs can be diverted from the command line to a log file, so that they can be displayed
// later (such as at the end of a playwright test run to help diagnose potential failures).
// Use this instead of loglevel whenever logging messages inside the app.
import * as fs from 'fs';
import * as path from 'path';
import loglevel from 'loglevel';
import { safeGetEnv } from './playwrightHelpers';
const USE_LOG_FILE = safeGetEnv('USE_LOG_FILE') === '1';
const LOG_FILE_DIR = safeGetEnv('LOG_FILE_DIR') ?? process.cwd();
const LOG_FILENAME = path.join(LOG_FILE_DIR, `${new Date().getTime()}.log`);
const logLevelNames = ['TRACE', 'DEBUG', 'INFO ', 'WARN ', 'ERROR'];
function _logger(
logFunc: (...args: unknown[]) => void,
level: loglevel.LogLevelNumbers,
...args: unknown[]
): void {
if (USE_LOG_FILE && loglevel.getLevel() >= level) {
for (const arg of args) {
try {
const lines =
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
JSON.stringify(arg, null, 2)?.split('\n') ?? `${arg}`.split('\n');
for (const line of lines) {
fs.appendFileSync(
LOG_FILENAME,
`${new Date().getTime()} ${logLevelNames[level]} ${line}\n`,
);
}
} catch {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
fs.appendFileSync(LOG_FILENAME, `${logLevelNames[level]} ${arg}\n`);
}
}
} else {
logFunc(...args);
}
}
export function debug(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.debug, loglevel.levels.DEBUG, ...args);
}
export function error(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.error, loglevel.levels.ERROR, ...args);
}
export function info(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.info, loglevel.levels.INFO, ...args);
}
export function log(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.info, loglevel.levels.INFO, ...args);
}
export function setLevel(
level: loglevel.LogLevelDesc,
persist?: boolean,
): void {
loglevel.setLevel(level, persist);
}
export function trace(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.trace, loglevel.levels.TRACE, ...args);
}
export function warn(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.warn, loglevel.levels.WARN, ...args);
}

View File

@ -0,0 +1,6 @@
export const IS_PLAYWRIGHT = safeGetEnv('PLAYWRIGHT_TEST') === '1';
export const PLAYWRIGHT_CONFIG = safeGetEnv('PLAYWRIGHT_CONFIG');
export function safeGetEnv(key: string): string | undefined {
return key in process.env ? process.env[key] : undefined;
}

View File

@ -0,0 +1,353 @@
jest.mock('./helpers');
jest.mock('./windowEvents');
jest.mock('./windowHelpers');
import { dialog, BrowserWindow, HandlerDetails, WebContents } from 'electron';
import { WindowOptions } from '../../../shared/src/options/model';
import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {
onNewWindowHelper,
onWillNavigate,
onWillPreventUnload,
}: {
onNewWindowHelper: (
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
details: Partial<HandlerDetails>,
parent?: BrowserWindow,
) => ReturnType<Parameters<WebContents['setWindowOpenHandler']>[0]>;
onWillNavigate: (
options: {
blockExternalUrls: boolean;
internalUrls?: string | RegExp;
targetUrl: string;
},
event: unknown,
urlToGo: string,
) => Promise<void>;
onWillPreventUnload: (event: unknown) => void;
} = jest.requireActual('./windowEvents');
import {
showNavigationBlockedMessage,
createAboutBlankWindow,
createNewTab,
} from './windowHelpers';
describe('onNewWindowHelper', () => {
const originalURL = 'https://medium.com/';
const internalURL = 'https://medium.com/topics/technology';
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
const foregroundDisposition = 'foreground-tab';
const backgroundDisposition = 'background-tab';
const baseOptions = {
autoHideMenuBar: true,
blockExternalUrls: false,
insecure: false,
name: 'TEST_APP',
targetUrl: originalURL,
zoom: 1.0,
} as WindowOptions;
const mockShowNavigationBlockedMessage: jest.SpyInstance =
showNavigationBlockedMessage as jest.Mock;
const mockCreateAboutBlank: jest.SpyInstance =
createAboutBlankWindow as jest.Mock;
const mockCreateNewTab: jest.SpyInstance = createNewTab as jest.Mock;
const mockLinkIsInternal: jest.SpyInstance = (
linkIsInternal as jest.Mock
).mockImplementation(() => true);
const mockNativeTabsSupported: jest.SpyInstance =
nativeTabsSupported as jest.Mock;
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
const setupWindow = jest.fn();
beforeEach(() => {
mockShowNavigationBlockedMessage
.mockReset()
.mockReturnValue(Promise.resolve(undefined));
mockCreateAboutBlank.mockReset();
mockCreateNewTab.mockReset();
mockLinkIsInternal.mockReset().mockReturnValue(true);
mockNativeTabsSupported.mockReset().mockReturnValue(false);
mockOpenExternal.mockReset();
setupWindow.mockReset();
});
afterAll(() => {
mockShowNavigationBlockedMessage.mockRestore();
mockCreateAboutBlank.mockRestore();
mockCreateNewTab.mockRestore();
mockLinkIsInternal.mockRestore();
mockNativeTabsSupported.mockRestore();
mockOpenExternal.mockRestore();
});
test('internal urls should not be handled', () => {
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: internalURL,
});
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('allow');
});
test('external urls should be opened externally', () => {
mockLinkIsInternal.mockReturnValue(false);
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: externalURL,
});
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(result.action).toEqual('deny');
});
test('external urls should be ignored if blockExternalUrls is true', () => {
mockLinkIsInternal.mockReturnValue(false);
const options = {
...baseOptions,
blockExternalUrls: true,
};
const result = onNewWindowHelper(options, setupWindow, {
url: externalURL,
});
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
test('tab disposition should be ignored if tabs are not enabled', () => {
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: internalURL,
disposition: foregroundDisposition,
});
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('allow');
});
test('tab disposition should be ignored if url is external', () => {
mockLinkIsInternal.mockReturnValue(false);
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: externalURL,
disposition: foregroundDisposition,
});
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(result.action).toEqual('deny');
});
test('foreground tabs with internal urls should be opened in the foreground', () => {
mockNativeTabsSupported.mockReturnValue(true);
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: internalURL,
disposition: foregroundDisposition,
});
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).toHaveBeenCalledWith(
baseOptions,
setupWindow,
internalURL,
true,
);
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
test('background tabs with internal urls should be opened in background tabs', () => {
mockNativeTabsSupported.mockReturnValue(true);
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: internalURL,
disposition: backgroundDisposition,
});
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).toHaveBeenCalledWith(
baseOptions,
setupWindow,
internalURL,
false,
);
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
test('about:blank urls should be handled', () => {
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: 'about:blank',
});
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
test('about:blank#blocked urls should be handled', () => {
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: 'about:blank#blocked',
});
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
test('about:blank#other urls should not be handled', () => {
const result = onNewWindowHelper(baseOptions, setupWindow, {
url: 'about:blank#other',
});
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('allow');
});
});
describe('onWillNavigate', () => {
const originalURL = 'https://medium.com/';
const internalURL = 'https://medium.com/topics/technology';
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
const mockShowNavigationBlockedMessage: jest.SpyInstance =
showNavigationBlockedMessage as jest.Mock;
const mockLinkIsInternal: jest.SpyInstance = linkIsInternal as jest.Mock;
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
const preventDefault = jest.fn();
beforeEach(() => {
mockShowNavigationBlockedMessage
.mockReset()
.mockReturnValue(Promise.resolve(undefined));
mockLinkIsInternal.mockReset().mockReturnValue(false);
mockOpenExternal.mockReset();
preventDefault.mockReset();
});
afterAll(() => {
mockShowNavigationBlockedMessage.mockRestore();
mockLinkIsInternal.mockRestore();
mockOpenExternal.mockRestore();
});
test('internal urls should not be handled', async () => {
mockLinkIsInternal.mockReturnValue(true);
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
const event = { preventDefault };
await onWillNavigate(options, event, internalURL);
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled();
});
test('external urls should be opened externally', async () => {
const options = {
blockExternalUrls: false,
targetUrl: originalURL,
};
const event = { preventDefault };
await onWillNavigate(options, event, externalURL);
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('external urls should be blocked if blockExternalUrls is true', async () => {
const options = {
blockExternalUrls: true,
targetUrl: originalURL,
};
const event = { preventDefault };
await onWillNavigate(options, event, externalURL);
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledTimes(1);
});
});
describe('onWillPreventUnload', () => {
const mockFromWebContents: jest.SpyInstance = jest
.spyOn(BrowserWindow, 'fromWebContents')
.mockImplementation(() => new BrowserWindow());
const mockShowDialog: jest.SpyInstance = jest.spyOn(
dialog,
'showMessageBoxSync',
);
const preventDefault: jest.SpyInstance = jest.fn();
beforeEach(() => {
mockFromWebContents.mockReset();
mockShowDialog.mockReset().mockReturnValue(undefined);
preventDefault.mockReset();
});
afterAll(() => {
mockFromWebContents.mockRestore();
mockShowDialog.mockRestore();
});
test('with no sender', () => {
const event = {};
onWillPreventUnload(event);
expect(mockFromWebContents).not.toHaveBeenCalled();
expect(mockShowDialog).not.toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled();
});
test('shows dialog and calls preventDefault on ok', () => {
mockShowDialog.mockReturnValue(0);
const event = { preventDefault, sender: {} };
onWillPreventUnload(event);
expect(mockFromWebContents).toHaveBeenCalledWith(event.sender);
expect(mockShowDialog).toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledWith();
});
test('shows dialog and does not call preventDefault on cancel', () => {
mockShowDialog.mockReturnValue(1);
const event = { preventDefault, sender: {} };
onWillPreventUnload(event);
expect(mockFromWebContents).toHaveBeenCalledWith(event.sender);
expect(mockShowDialog).toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,188 @@
import {
dialog,
BrowserWindow,
Event,
WebContents,
HandlerDetails,
} from 'electron';
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
import * as log from './loggingHelper';
import {
createAboutBlankWindow,
createNewTab,
injectCSS,
sendParamsOnDidFinishLoad,
setProxyRules,
showNavigationBlockedMessage,
} from './windowHelpers';
import { WindowOptions } from '../../../shared/src/options/model';
type NewWindowHandlerResult = ReturnType<
Parameters<WebContents['setWindowOpenHandler']>[0]
>;
export function onNewWindow(
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
details: HandlerDetails,
parent?: BrowserWindow,
): NewWindowHandlerResult {
log.debug('onNewWindow', {
details,
});
return onNewWindowHelper(
options,
setupWindow,
details,
nativeTabsSupported() ? undefined : parent,
);
}
export function onNewWindowHelper(
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
details: HandlerDetails,
parent?: BrowserWindow,
): NewWindowHandlerResult {
log.debug('onNewWindowHelper', {
options,
details,
});
try {
if (
!linkIsInternal(
options.targetUrl,
details.url,
options.internalUrls,
options.strictInternalUrls,
)
) {
if (options.blockExternalUrls) {
showNavigationBlockedMessage(
`Navigation to external URL blocked by options: ${details.url}`,
)
.then(() => {
// blockExternalURL(details.url).then(resolve).catch((err: unknown) => {
// log.error('blockExternalURL', err);
// });
})
.catch((err: unknown) => {
throw err;
});
return { action: 'deny' };
} else {
openExternal(details.url).catch((err: unknown) => {
log.error('openExternal', err);
});
return { action: 'deny' };
}
}
// Normally the following would be:
// if (urlToGo.startsWith('about:blank'))...
// But due to a bug we resolved in https://github.com/nativefier/nativefier/issues/1197
// Some sites use about:blank#something to use as placeholder windows to fill
// with content via JavaScript. So we'll stay specific for now...
else if (['about:blank', 'about:blank#blocked'].includes(details.url)) {
createAboutBlankWindow(
options,
setupWindow,
nativeTabsSupported() ? undefined : parent,
);
return { action: 'deny' };
} else if (nativeTabsSupported()) {
createNewTab(
options,
setupWindow,
details.url,
details.disposition === 'foreground-tab',
);
return { action: 'deny' };
}
return { action: 'allow' };
} catch (err: unknown) {
return { action: 'deny' };
}
}
export function onWillNavigate(
options: WindowOptions,
event: Event,
urlToGo: string,
): Promise<void> {
log.debug('onWillNavigate', urlToGo);
if (
!linkIsInternal(
options.targetUrl,
urlToGo,
options.internalUrls,
options.strictInternalUrls,
)
) {
event.preventDefault();
if (options.blockExternalUrls) {
return new Promise((resolve) => {
showNavigationBlockedMessage(
`Navigation to external URL blocked by options: ${urlToGo}`,
)
.then(() => resolve())
.catch((err: unknown) => {
throw err;
});
});
} else {
return openExternal(urlToGo);
}
}
return Promise.resolve(undefined);
}
export function onWillPreventUnload(
event: Event & { sender?: WebContents },
): void {
log.debug('onWillPreventUnload', event);
const webContents = event.sender;
if (!webContents) {
return;
}
const browserWindow =
BrowserWindow.fromWebContents(webContents) ??
BrowserWindow.getFocusedWindow();
if (browserWindow) {
const choice = dialog.showMessageBoxSync(browserWindow, {
type: 'question',
buttons: ['Proceed', 'Stay'],
message:
'You may have unsaved changes, are you sure you want to proceed?',
title: 'Changes you made may not be saved.',
defaultId: 0,
cancelId: 1,
});
if (choice === 0) {
event.preventDefault();
}
}
}
export function setupNativefierWindow(
options: WindowOptions,
window: BrowserWindow,
): void {
if (options.proxyRules) {
setProxyRules(window, options.proxyRules);
}
injectCSS(window);
window.webContents.on('will-navigate', (event: Event, url: string) => {
onWillNavigate(options, event, url).catch((err) => {
log.error('window.webContents.on.will-navigate ERROR', err);
event.preventDefault();
});
});
window.webContents.on('will-prevent-unload', onWillPreventUnload);
sendParamsOnDidFinishLoad(options, window);
}

View File

@ -0,0 +1,292 @@
import { dialog, BrowserWindow } from 'electron';
jest.mock('loglevel');
import { error } from 'loglevel';
import { WindowOptions } from '../../../shared/src/options/model';
jest.mock('./helpers');
import { getCSSToInject } from './helpers';
jest.mock('./windowEvents');
import { clearAppData, createNewTab, injectCSS } from './windowHelpers';
describe('clearAppData', () => {
let window: BrowserWindow;
let mockClearCache: jest.SpyInstance;
let mockClearStorageData: jest.SpyInstance;
const mockShowDialog: jest.SpyInstance = jest.spyOn(dialog, 'showMessageBox');
beforeEach(() => {
window = new BrowserWindow();
mockClearCache = jest.spyOn(window.webContents.session, 'clearCache');
mockClearStorageData = jest.spyOn(
window.webContents.session,
'clearStorageData',
);
mockShowDialog.mockReset().mockResolvedValue(undefined);
});
afterAll(() => {
mockClearCache.mockRestore();
mockClearStorageData.mockRestore();
mockShowDialog.mockRestore();
});
test('will not clear app data if dialog canceled', async () => {
mockShowDialog.mockResolvedValue(1);
await clearAppData(window);
expect(mockShowDialog).toHaveBeenCalledTimes(1);
expect(mockClearCache).not.toHaveBeenCalled();
expect(mockClearStorageData).not.toHaveBeenCalled();
});
test('will clear app data if ok is clicked', async () => {
mockShowDialog.mockResolvedValue(0);
await clearAppData(window);
expect(mockShowDialog).toHaveBeenCalledTimes(1);
expect(mockClearCache).not.toHaveBeenCalledTimes(1);
expect(mockClearStorageData).not.toHaveBeenCalledTimes(1);
});
});
describe('createNewTab', () => {
// const window = new BrowserWindow();
const options: WindowOptions = {
autoHideMenuBar: true,
blockExternalUrls: false,
insecure: false,
name: 'Test App',
targetUrl: 'https://github.com/nativefier/natifefier',
zoom: 1.0,
} as WindowOptions;
const setupWindow = jest.fn();
const url = 'https://github.com/nativefier/nativefier';
const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn(
BrowserWindow.prototype,
'addTabbedWindow',
);
const mockFocus: jest.SpyInstance = jest.spyOn(
BrowserWindow.prototype,
'focus',
);
const mockLoadURL: jest.SpyInstance = jest.spyOn(
BrowserWindow.prototype,
'loadURL',
);
test('creates new foreground tab', () => {
const foreground = true;
const tab = createNewTab(options, setupWindow, url, foreground);
expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab);
expect(setupWindow).toHaveBeenCalledWith(options, tab);
expect(mockLoadURL).toHaveBeenCalledWith(url);
expect(mockFocus).not.toHaveBeenCalled();
});
test('creates new background tab', () => {
const foreground = false;
const tab = createNewTab(
options,
setupWindow,
url,
foreground,
// window
);
expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab);
expect(setupWindow).toHaveBeenCalledWith(options, tab);
expect(mockLoadURL).toHaveBeenCalledWith(url);
expect(mockFocus).toHaveBeenCalledTimes(1);
});
});
describe('injectCSS', () => {
jest.setTimeout(10000);
const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock;
const mockLogError: jest.SpyInstance = error as jest.Mock;
const css = 'body { color: white; }';
let responseHeaders: Record<string, string[]>;
beforeEach(() => {
mockGetCSSToInject.mockReset().mockReturnValue('');
mockLogError.mockReset();
responseHeaders = { 'x-header': ['value'], 'content-type': ['test/other'] };
});
afterAll(() => {
mockGetCSSToInject.mockRestore();
mockLogError.mockRestore();
});
test('will not inject if getCSSToInject is empty', () => {
const window = new BrowserWindow();
const mockWebContentsInsertCSS: jest.SpyInstance = jest
.spyOn(window.webContents, 'insertCSS')
.mockResolvedValue('');
jest
.spyOn(window.webContents, 'getURL')
.mockReturnValue('https://example.com');
injectCSS(window);
expect(mockGetCSSToInject).toHaveBeenCalled();
expect(mockWebContentsInsertCSS).not.toHaveBeenCalled();
});
test('will inject on did-navigate + onResponseStarted', () => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
const mockWebContentsInsertCSS: jest.SpyInstance = jest
.spyOn(window.webContents, 'insertCSS')
.mockResolvedValue('');
jest
.spyOn(window.webContents, 'getURL')
.mockReturnValue('https://example.com');
injectCSS(window);
expect(mockGetCSSToInject).toHaveBeenCalled();
window.webContents.emit('did-navigate');
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send('onResponseStarted', {
responseHeaders,
webContents: window.webContents,
});
expect(mockWebContentsInsertCSS).toHaveBeenCalledWith(css);
});
test.each<string>(['application/json', 'font/woff2', 'image/png'])(
'will not inject for content-type %s',
(contentType: string) => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
const mockWebContentsInsertCSS: jest.SpyInstance = jest
.spyOn(window.webContents, 'insertCSS')
.mockResolvedValue('');
jest
.spyOn(window.webContents, 'getURL')
.mockReturnValue('https://example.com');
responseHeaders['content-type'] = [contentType];
injectCSS(window);
expect(mockGetCSSToInject).toHaveBeenCalled();
expect(window.webContents.emit('did-navigate')).toBe(true);
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send('onResponseStarted', {
responseHeaders,
webContents: window.webContents,
url: `test-${contentType}`,
});
// insertCSS will still run once for the did-navigate
expect(mockWebContentsInsertCSS).not.toHaveBeenCalled();
},
);
test.each<string>(['text/html'])(
'will inject for content-type %s',
(contentType: string) => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
const mockWebContentsInsertCSS: jest.SpyInstance = jest
.spyOn(window.webContents, 'insertCSS')
.mockResolvedValue('');
jest
.spyOn(window.webContents, 'getURL')
.mockReturnValue('https://example.com');
responseHeaders['content-type'] = [contentType];
injectCSS(window);
expect(mockGetCSSToInject).toHaveBeenCalled();
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send('onResponseStarted', {
responseHeaders,
webContents: window.webContents,
url: `test-${contentType}`,
});
expect(mockWebContentsInsertCSS).toHaveBeenCalledTimes(1);
},
);
test.each<string>(['image', 'script', 'stylesheet', 'xhr'])(
'will not inject for resource type %s',
(resourceType: string) => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
const mockWebContentsInsertCSS: jest.SpyInstance = jest
.spyOn(window.webContents, 'insertCSS')
.mockResolvedValue('');
jest
.spyOn(window.webContents, 'getURL')
.mockReturnValue('https://example.com');
injectCSS(window);
expect(mockGetCSSToInject).toHaveBeenCalled();
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send('onResponseStarted', {
responseHeaders,
webContents: window.webContents,
resourceType,
url: `test-${resourceType}`,
});
// insertCSS will still run once for the did-navigate
expect(mockWebContentsInsertCSS).not.toHaveBeenCalled();
},
);
test.each<string>(['html', 'other'])(
'will inject for resource type %s',
(resourceType: string) => {
mockGetCSSToInject.mockReturnValue(css);
const window = new BrowserWindow();
const mockWebContentsInsertCSS: jest.SpyInstance = jest
.spyOn(window.webContents, 'insertCSS')
.mockResolvedValue('');
jest
.spyOn(window.webContents, 'getURL')
.mockReturnValue('https://example.com');
injectCSS(window);
expect(mockGetCSSToInject).toHaveBeenCalled();
window.webContents.emit('did-navigate');
mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined);
// @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window.webContents.session.webRequest.send('onResponseStarted', {
responseHeaders,
webContents: window.webContents,
resourceType,
url: `test-${resourceType}`,
});
expect(mockWebContentsInsertCSS).toHaveBeenCalledTimes(1);
},
);
});

View File

@ -0,0 +1,365 @@
import path from 'path';
import {
dialog,
BrowserWindow,
BrowserWindowConstructorOptions,
Event,
MessageBoxReturnValue,
WebPreferences,
OnResponseStartedListenerDetails,
} from 'electron';
import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers';
import * as log from './loggingHelper';
import { TrayValue, WindowOptions } from '../../../shared/src/options/model';
import { randomUUID } from 'crypto';
const ZOOM_INTERVAL = 0.1;
export function adjustWindowZoom(adjustment: number): void {
withFocusedWindow((focusedWindow: BrowserWindow) => {
focusedWindow.webContents.zoomFactor =
focusedWindow.webContents.zoomFactor + adjustment;
});
}
export function showNavigationBlockedMessage(
message: string,
): Promise<MessageBoxReturnValue> {
return new Promise((resolve, reject) => {
withFocusedWindow((focusedWindow) => {
dialog
.showMessageBox(focusedWindow, {
message,
type: 'error',
title: 'Navigation blocked',
})
.then((result) => resolve(result))
.catch((err) => {
reject(err);
});
});
});
}
export async function clearAppData(window: BrowserWindow): Promise<void> {
const response = await dialog.showMessageBox(window, {
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?',
});
if (response.response !== 0) {
return;
}
await clearCache(window);
}
export async function clearCache(window: BrowserWindow): Promise<void> {
const { session } = window.webContents;
await session.clearStorageData();
await session.clearCache();
}
export function createAboutBlankWindow(
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
parent?: BrowserWindow,
): BrowserWindow {
const window = createNewWindow(
{ ...options, show: false },
setupWindow,
'about:blank',
nativeTabsSupported() ? undefined : parent,
);
window.webContents.once('did-stop-loading', () => {
if (window.webContents.getURL() === 'about:blank') {
window.close();
} else {
window.show();
}
});
return window;
}
export function createNewTab(
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
url: string,
foreground: boolean,
): BrowserWindow | undefined {
const focusedWindow = BrowserWindow.getFocusedWindow();
log.debug('createNewTab', {
url,
foreground,
focusedWindow,
});
return withFocusedWindow((focusedWindow) => {
const newTab = createNewWindow(options, setupWindow, url);
log.debug('createNewTab.withFocusedWindow', { focusedWindow, newTab });
focusedWindow.addTabbedWindow(newTab);
if (!foreground) {
focusedWindow.focus();
}
return newTab;
});
}
export function createNewWindow(
options: WindowOptions,
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
url: string,
parent?: BrowserWindow,
): BrowserWindow {
log.debug('createNewWindow', {
url,
parent,
});
const window = new BrowserWindow({
parent: nativeTabsSupported() ? undefined : parent,
...getDefaultWindowOptions(options),
});
setupWindow(options, window);
window.loadURL(url).catch((err) => log.error('window.loadURL ERROR', err));
return window;
}
export function getCurrentURL(): string {
return withFocusedWindow((focusedWindow) =>
focusedWindow.webContents.getURL(),
) as unknown as string;
}
export function getDefaultWindowOptions(
options: WindowOptions,
): BrowserWindowConstructorOptions {
const browserwindowOptions: BrowserWindowConstructorOptions = {
...options.browserwindowOptions,
};
// We're going to remove this and merge it separately into DEFAULT_WINDOW_OPTIONS.webPreferences
// Otherwise the browserwindowOptions.webPreferences object will completely replace the
// webPreferences specified in the DEFAULT_WINDOW_OPTIONS with itself
delete browserwindowOptions.webPreferences;
const webPreferences: WebPreferences = {
...(options.browserwindowOptions?.webPreferences ?? {}),
};
const defaultOptions: BrowserWindowConstructorOptions = {
autoHideMenuBar: options.autoHideMenuBar,
fullscreenable: true,
tabbingIdentifier: nativeTabsSupported()
? options.tabbingIdentifier ?? randomUUID()
: undefined,
title: options.name,
webPreferences: {
javascript: true,
nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com
preload: path.join(__dirname, 'preload.js'),
plugins: true,
sandbox: false, // https://www.electronjs.org/blog/electron-20-0#default-changed-renderers-without-nodeintegration-true-are-sandboxed-by-default
webSecurity: !options.insecure,
zoomFactor: options.zoom,
// `contextIsolation` was switched to true in Electron 12, which:
// 1. Breaks access to global variables in `--inject`-ed scripts:
// https://github.com/nativefier/nativefier/issues/1269
// 2. Might break notifications under Windows, although this was refuted:
// https://github.com/nativefier/nativefier/issues/1292
// So, it was flipped to false in https://github.com/nativefier/nativefier/pull/1308
//
// If attempting to set it back to `true` (for security),
// do test exhaustively these two areas, and more.
contextIsolation: false,
...webPreferences,
},
...browserwindowOptions,
};
log.debug('getDefaultWindowOptions', {
options,
webPreferences,
defaultOptions,
});
return defaultOptions;
}
export function goBack(): void {
log.debug('onGoBack');
withFocusedWindow((focusedWindow) => {
focusedWindow.webContents.goBack();
});
}
export function goForward(): void {
log.debug('onGoForward');
withFocusedWindow((focusedWindow) => {
focusedWindow.webContents.goForward();
});
}
export function goToURL(url: string): Promise<void> | undefined {
return withFocusedWindow((focusedWindow) => focusedWindow.loadURL(url));
}
export function hideWindow(
window: BrowserWindow,
event: Event,
fastQuit: boolean,
tray: TrayValue,
): void {
if (isOSX() && !fastQuit) {
// this is called when exiting from clicking the cross button on the window
event.preventDefault();
window.hide();
} else if (!fastQuit && tray !== 'false') {
event.preventDefault();
window.hide();
}
// will close the window on other platforms
}
export function injectCSS(browserWindow: BrowserWindow): void {
const cssToInject = getCSSToInject();
if (!cssToInject) {
return;
}
browserWindow.webContents.on('did-navigate', () => {
log.debug(
'browserWindow.webContents.did-navigate',
browserWindow.webContents.getURL(),
);
browserWindow.webContents
.insertCSS(cssToInject)
.catch((err: unknown) =>
log.error('browserWindow.webContents.insertCSS', err),
);
// We must inject css early enough; so onResponseStarted is a good place.
browserWindow.webContents.session.webRequest.onResponseStarted(
{ urls: [] }, // Pass an empty filter list; null will not match _any_ urls
(details: OnResponseStartedListenerDetails): void => {
log.debug('onResponseStarted', {
resourceType: details.resourceType,
url: details.url,
});
injectCSSIntoResponse(details, cssToInject).catch((err: unknown) => {
log.error('injectCSSIntoResponse ERROR', err);
});
},
);
});
}
function injectCSSIntoResponse(
details: OnResponseStartedListenerDetails,
cssToInject: string,
): Promise<string | undefined> {
const contentType =
details.responseHeaders && 'content-type' in details.responseHeaders
? details.responseHeaders['content-type'][0]
: undefined;
log.debug('injectCSSIntoResponse', { details, cssToInject, contentType });
// We go with a denylist rather than a whitelist (e.g. only text/html)
// to avoid "whoops I didn't think this should have been CSS-injected" cases
const nonInjectableContentTypes = [
/application\/.*/,
/font\/.*/,
/image\/.*/,
];
const nonInjectableResourceTypes = ['image', 'script', 'stylesheet', 'xhr'];
if (
(contentType &&
nonInjectableContentTypes.filter((x) => {
const matches = x.exec(contentType);
return matches && matches?.length > 0;
})?.length > 0) ||
nonInjectableResourceTypes.includes(details.resourceType) ||
!details.webContents
) {
log.debug(
`Skipping CSS injection for:\n${details.url}\nwith resourceType ${
details.resourceType
} and content-type ${contentType as string}`,
);
return Promise.resolve(undefined);
}
log.debug(
`Injecting CSS for:\n${details.url}\nwith resourceType ${
details.resourceType
} and content-type ${contentType as string}`,
);
return details.webContents.insertCSS(cssToInject);
}
export function sendParamsOnDidFinishLoad(
options: WindowOptions,
window: BrowserWindow,
): void {
window.webContents.on('did-finish-load', () => {
log.debug(
'sendParamsOnDidFinishLoad.window.webContents.did-finish-load',
window.webContents.getURL(),
);
// In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron.
// See https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128
// and https://github.com/electron/electron/pull/12679
window.webContents
.setVisualZoomLevelLimits(1, 3)
.catch((err) => log.error('webContents.setVisualZoomLevelLimits', err));
window.webContents.send('params', JSON.stringify(options));
});
}
export function setProxyRules(
window: BrowserWindow,
proxyRules?: string,
): void {
window.webContents.session
.setProxy({
proxyRules,
pacScript: '',
proxyBypassRules: '',
})
.catch((err) => log.error('session.setProxy ERROR', err));
}
export function withFocusedWindow<T>(
block: (window: BrowserWindow) => T,
): T | undefined {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
return block(focusedWindow);
}
return undefined;
}
export function zoomOut(): void {
log.debug('zoomOut');
adjustWindowZoom(-ZOOM_INTERVAL);
}
export function zoomReset(options: { zoom?: number }): void {
log.debug('zoomReset');
withFocusedWindow((focusedWindow) => {
focusedWindow.webContents.zoomFactor = options.zoom ?? 1.0;
});
}
export function zoomIn(): void {
log.debug('zoomIn');
adjustWindowZoom(ZOOM_INTERVAL);
}

View File

@ -1,32 +1,116 @@
import 'source-map-support/register'; import 'source-map-support/register';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import * as path from 'path';
import { import electron, {
app, app,
crashReporter,
globalShortcut,
BrowserWindow,
dialog, dialog,
globalShortcut,
systemPreferences,
BrowserWindow,
Event,
} from 'electron'; } from 'electron';
import electronDownload from 'electron-dl'; import electronDownload from 'electron-dl';
import { createLoginWindow } from './components/loginWindow'; import { createLoginWindow } from './components/loginWindow';
import { createMainWindow } from './components/mainWindow'; import {
createMainWindow,
saveAppArgs,
APP_ARGS_FILE_PATH,
} from './components/mainWindow';
import { createTrayIcon } from './components/trayIcon'; import { createTrayIcon } from './components/trayIcon';
import { isOSX } from './helpers/helpers'; import {
isOSX,
isWayland,
isWindows,
removeUserAgentSpecifics,
} from './helpers/helpers';
import { inferFlashPath } from './helpers/inferFlash'; import { inferFlashPath } from './helpers/inferFlash';
import * as log from './helpers/loggingHelper';
import {
IS_PLAYWRIGHT,
PLAYWRIGHT_CONFIG,
safeGetEnv,
} from './helpers/playwrightHelpers';
import { OutputOptions } from '../../shared/src/options/model';
// Entrypoint for Squirrel, a windows update framework. See https://github.com/jiahaog/nativefier/pull/744 // Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
if (require('electron-squirrel-startup')) { if (require('electron-squirrel-startup')) {
app.exit(); app.exit();
} }
const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json'); if (process.argv.indexOf('--verbose') > -1 || safeGetEnv('VERBOSE') === '1') {
const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8')); log.setLevel('DEBUG');
process.traceDeprecation = true;
process.traceProcessWarnings = true;
process.argv.slice(1);
}
const OLD_BUILD_WARNING_THRESHOLD_DAYS = 60; let mainWindow: BrowserWindow;
const appArgs =
IS_PLAYWRIGHT && PLAYWRIGHT_CONFIG
? (JSON.parse(PLAYWRIGHT_CONFIG) as OutputOptions)
: (JSON.parse(
fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'),
) as OutputOptions);
log.debug('appArgs', appArgs);
// Do this relatively early so that we can start storing appData with the app
if (appArgs.portable) {
log.debug(
'App was built as portable; setting appData and userData to the app folder: ',
path.resolve(path.join(__dirname, '..', 'appData')),
);
app.setPath('appData', path.join(__dirname, '..', 'appData'));
app.setPath('userData', path.join(__dirname, '..', 'appData'));
}
if (!appArgs.userAgentHonest) {
if (appArgs.userAgent) {
app.userAgentFallback = appArgs.userAgent;
} else {
app.userAgentFallback = removeUserAgentSpecifics(
app.userAgentFallback,
app.getName(),
app.getVersion(),
);
}
}
// this step is required to allow app names to be displayed correctly in notifications on windows
// https://www.electronjs.org/docs/latest/api/app#appsetappusermodelidid-windows
// https://www.electronjs.org/docs/latest/tutorial/notifications#windows
if (isWindows()) {
app.setAppUserModelId(app.getName());
}
const urlArgv = process.argv.filter((a) => a.startsWith('http'));
// Take in a URL on the command line as an override
if (urlArgv.length > 0) {
const maybeUrl = urlArgv[0];
try {
new URL(maybeUrl);
appArgs.targetUrl = maybeUrl;
log.info('Loading override URL passed as argument:', maybeUrl);
} catch (err: unknown) {
log.error(
'Not loading override URL passed as argument, because failed to parse:',
maybeUrl,
err,
);
}
}
// Nativefier is a browser, and an old browser is an insecure / badly-performant one.
// Given our builder/app design, we currently don't have an easy way to offer
// upgrades from the app themselves (like browsers do).
// As a workaround, we ask for a manual upgrade & re-build if the build is old.
// The period in days is chosen to be not too small to be exceedingly annoying,
// but not too large to be exceedingly insecure.
const OLD_BUILD_WARNING_THRESHOLD_DAYS = 90;
const OLD_BUILD_WARNING_THRESHOLD_MS = const OLD_BUILD_WARNING_THRESHOLD_MS =
OLD_BUILD_WARNING_THRESHOLD_DAYS * 24 * 60 * 60 * 1000; OLD_BUILD_WARNING_THRESHOLD_DAYS * 24 * 60 * 60 * 1000;
@ -34,18 +118,24 @@ const fileDownloadOptions = { ...appArgs.fileDownloadOptions };
electronDownload(fileDownloadOptions); electronDownload(fileDownloadOptions);
if (appArgs.processEnvs) { if (appArgs.processEnvs) {
let processEnvs: Record<string, string> =
appArgs.processEnvs as unknown as Record<string, string>;
// This is compatibility if just a string was passed. // This is compatibility if just a string was passed.
if (typeof appArgs.processEnvs === 'string') { if (typeof appArgs.processEnvs === 'string') {
try {
processEnvs = JSON.parse(appArgs.processEnvs) as Record<string, string>;
} catch {
// This wasn't JSON. Fall back to the old code
processEnvs = {};
process.env.processEnvs = appArgs.processEnvs; process.env.processEnvs = appArgs.processEnvs;
} else { }
Object.keys(appArgs.processEnvs).forEach((key) => { }
/* eslint-env node */ Object.keys(processEnvs)
process.env[key] = appArgs.processEnvs[key]; .filter((key) => key !== undefined)
.forEach((key) => {
process.env[key] = processEnvs[key];
}); });
} }
}
let mainWindow: BrowserWindow;
if (typeof appArgs.flashPluginDir === 'string') { if (typeof appArgs.flashPluginDir === 'string') {
app.commandLine.appendSwitch('ppapi-flash-path', appArgs.flashPluginDir); app.commandLine.appendSwitch('ppapi-flash-path', appArgs.flashPluginDir);
@ -71,7 +161,10 @@ if (appArgs.enableEs3Apis) {
} }
if (appArgs.diskCacheSize) { if (appArgs.diskCacheSize) {
app.commandLine.appendSwitch('disk-cache-size', appArgs.diskCacheSize); app.commandLine.appendSwitch(
'disk-cache-size',
appArgs.diskCacheSize.toString(),
);
} }
if (appArgs.basicAuthUsername) { if (appArgs.basicAuthUsername) {
@ -88,32 +181,43 @@ if (appArgs.basicAuthPassword) {
); );
} }
const isRunningMacos = isOSX(); if (isWayland()) {
let currentBadgeCount = 0; app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
const setDockBadge = isRunningMacos
? (count: number, bounce = false) => {
app.dock.setBadge(count.toString());
if (bounce && count > currentBadgeCount) app.dock.bounce();
currentBadgeCount = count;
} }
: () => undefined;
if (appArgs.lang) {
const langParts = appArgs.lang.split(',');
// Convert locales to languages, because for some reason locales don't work. Stupid Chromium
const langPartsParsed = Array.from(
// Convert to set to dedupe in case something like "en-GB,en-US" was passed
new Set(langParts.map((l) => l.split('-')[0])),
);
const langFlag = langPartsParsed.join(',');
log.debug('Setting --lang flag to', langFlag);
app.commandLine.appendSwitch('--lang', langFlag);
}
let currentBadgeCount = 0;
const setDockBadge = isOSX()
? (count?: number | string, bounce = false): void => {
if (count !== undefined) {
app.dock.setBadge(count.toString());
if (bounce && typeof count === 'number' && count > currentBadgeCount)
app.dock.bounce();
currentBadgeCount = typeof count === 'number' ? count : 0;
}
}
: (): void => undefined;
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (!isOSX() || appArgs.fastQuit) { log.debug('app.window-all-closed');
if (!isOSX() || appArgs.fastQuit || IS_PLAYWRIGHT) {
app.quit(); app.quit();
} }
}); });
app.on('activate', (event, hasVisibleWindows) => {
if (isOSX()) {
// this is called when the dock is clicked
if (!hasVisibleWindows) {
mainWindow.show();
}
}
});
app.on('before-quit', () => { app.on('before-quit', () => {
log.debug('app.before-quit');
// not fired when the close button on the window is clicked // not fired when the close button on the window is clicked
if (isOSX()) { if (isOSX()) {
// need to force a quit as a workaround here to simulate the osx app hiding behaviour // need to force a quit as a workaround here to simulate the osx app hiding behaviour
@ -125,23 +229,73 @@ app.on('before-quit', () => {
} }
}); });
if (appArgs.crashReporter) { app.on('will-quit', (event) => {
app.on('will-finish-launching', () => { log.debug('app.will-quit', event);
crashReporter.start({
companyName: appArgs.companyName || '',
productName: appArgs.name,
submitURL: appArgs.crashReporter,
uploadToServer: true,
}); });
app.on('quit', (event, exitCode) => {
log.debug('app.quit', { event, exitCode });
});
app.on('will-finish-launching', () => {
log.debug('app.will-finish-launching');
});
app.on('open-url', (event, url) => {
log.debug('app.open-url', { event, url });
event.preventDefault();
if (mainWindow) {
mainWindow.webContents.send('open-url', url);
}
});
if (appArgs.widevine) {
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
app.on('widevine-ready', (version: string, lastVersion: string) => {
log.debug('app.widevine-ready', { version, lastVersion });
onReady().catch((err) => log.error('onReady ERROR', err));
});
app.on(
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
'widevine-update-pending',
(currentVersion: string, pendingVersion: string) => {
log.debug('app.widevine-update-pending', {
currentVersion,
pendingVersion,
});
},
);
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
app.on('widevine-error', (error: Error) => {
log.error('app.widevine-error', error);
});
} else {
app.on('ready', () => {
log.debug('ready');
onReady().catch((err) => log.error('onReady ERROR', err));
}); });
} }
app.on('activate', (event: electron.Event, hasVisibleWindows: boolean) => {
log.debug('app.activate', { event, hasVisibleWindows });
if (isOSX() && !IS_PLAYWRIGHT) {
// this is called when the dock is clicked
if (!hasVisibleWindows) {
mainWindow.show();
}
}
});
// quit if singleInstance mode and there's already another instance running // quit if singleInstance mode and there's already another instance running
const shouldQuit = appArgs.singleInstance && !app.requestSingleInstanceLock(); const shouldQuit = appArgs.singleInstance && !app.requestSingleInstanceLock();
if (shouldQuit) { if (shouldQuit) {
app.quit(); app.quit();
} else { } else {
app.on('second-instance', () => { app.on('second-instance', () => {
log.debug('app.second-instance');
if (mainWindow) { if (mainWindow) {
if (!mainWindow.isVisible()) { if (!mainWindow.isVisible()) {
// try // try
@ -154,9 +308,43 @@ if (shouldQuit) {
mainWindow.focus(); mainWindow.focus();
} }
}); });
}
app.on('new-window-for-tab', (event: Event) => {
log.debug('app.new-window-for-tab', { event });
if (mainWindow) {
mainWindow.emit('new-window-for-tab', event);
}
});
app.on(
'login',
(
event,
webContents,
request,
authInfo,
callback: (username?: string, password?: string) => void,
) => {
log.debug('app.login', { event, request });
// for http authentication
event.preventDefault();
if (appArgs.basicAuthUsername && appArgs.basicAuthPassword) {
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
} else {
createLoginWindow(
callback,
// mainWindow
).catch((err) => log.error('createLoginWindow ERROR', err));
}
},
);
async function onReady(): Promise<void> {
// Warning: `mainWindow` below is the *global* unique `mainWindow`, created at init time
mainWindow = await createMainWindow(appArgs, setDockBadge);
app.on('ready', () => {
mainWindow = createMainWindow(appArgs, app.quit.bind(this), setDockBadge);
createTrayIcon(appArgs, mainWindow); createTrayIcon(appArgs, mainWindow);
// Register global shortcuts // Register global shortcuts
@ -164,40 +352,107 @@ if (shouldQuit) {
appArgs.globalShortcuts.forEach((shortcut) => { appArgs.globalShortcuts.forEach((shortcut) => {
globalShortcut.register(shortcut.key, () => { globalShortcut.register(shortcut.key, () => {
shortcut.inputEvents.forEach((inputEvent) => { shortcut.inputEvents.forEach((inputEvent) => {
// @ts-expect-error without including electron in our models, these will never match
mainWindow.webContents.sendInputEvent(inputEvent); mainWindow.webContents.sendInputEvent(inputEvent);
}); });
}); });
}); });
if (isOSX() && appArgs.accessibilityPrompt) {
const mediaKeys = [
'MediaPlayPause',
'MediaNextTrack',
'MediaPreviousTrack',
'MediaStop',
];
const globalShortcutsKeys = appArgs.globalShortcuts.map((g) => g.key);
const mediaKeyWasSet = globalShortcutsKeys.find((g) =>
mediaKeys.includes(g),
);
if (
mediaKeyWasSet &&
!systemPreferences.isTrustedAccessibilityClient(false)
) {
// Since we're trying to set global keyboard shortcuts for media keys, we need to prompt
// the user for permission on Mac.
// For reference:
// https://www.electronjs.org/docs/api/global-shortcut?q=MediaPlayPause#globalshortcutregisteraccelerator-callback
const accessibilityPromptResult = dialog.showMessageBoxSync(
mainWindow,
{
type: 'question',
message: 'Accessibility Permissions Needed',
buttons: ['Yes', 'No', 'No and never ask again'],
defaultId: 0,
detail:
`${appArgs.name} would like to use one or more of your keyboard's media keys (start, stop, next track, or previous track) to control it.\n\n` +
`Would you like Mac OS to ask for your permission to do so?\n\n` +
`If so, you will need to restart ${appArgs.name} after granting permissions for these keyboard shortcuts to begin working.`,
},
);
switch (accessibilityPromptResult) {
// User clicked Yes, prompt for accessibility
case 0:
systemPreferences.isTrustedAccessibilityClient(true);
break;
// User cliecked Never Ask Me Again, save that info
case 2:
appArgs.accessibilityPrompt = false;
saveAppArgs(appArgs);
break;
// User clicked No
default:
break;
}
}
}
} }
if ( if (
!appArgs.disableOldBuildWarning && !appArgs.disableOldBuildWarning &&
new Date().getTime() - appArgs.buildDate > OLD_BUILD_WARNING_THRESHOLD_MS new Date().getTime() - appArgs.buildDate > OLD_BUILD_WARNING_THRESHOLD_MS
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises const oldBuildWarningText =
dialog.showMessageBox(null, { appArgs.oldBuildWarningText ||
'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is insecure to keep using an old version of it. Please upgrade Nativefier and rebuild this app.';
dialog
.showMessageBox(mainWindow, {
type: 'warning', type: 'warning',
message: 'Old build detected', message: 'Old build detected',
detail: detail: oldBuildWarningText,
'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is dangerous to keep using an old version of it. You should rebuild this app with a recent Electron. Using the latest Nativefier will default to it, or you can pass it manually.', })
}); .catch((err) => log.error('dialog.showMessageBox ERROR', err));
}
});
} }
app.on('new-window-for-tab', () => { if (appArgs.targetUrl) {
mainWindow.emit('new-tab'); await mainWindow.loadURL(appArgs.targetUrl);
});
app.on('login', (event, webContents, request, authInfo, callback) => {
// for http authentication
event.preventDefault();
if (
appArgs.basicAuthUsername !== null &&
appArgs.basicAuthPassword !== null
) {
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
} else {
createLoginWindow(callback);
} }
}
app.on(
'accessibility-support-changed',
(event: Event, accessibilitySupportEnabled: boolean) => {
log.debug('app.accessibility-support-changed', {
event,
accessibilitySupportEnabled,
});
},
);
app.on(
'activity-was-continued',
(event: Event, type: string, userInfo: unknown) => {
log.debug('app.activity-was-continued', { event, type, userInfo });
},
);
app.on('browser-window-blur', () => {
log.debug('app.browser-window-blur');
});
app.on('browser-window-created', () => {
log.debug('app.browser-window-created');
});
app.on('browser-window-focus', () => {
log.debug('app.browser-window-focus');
}); });

160
app/src/mocks/electron.ts Normal file
View File

@ -0,0 +1,160 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { EventEmitter } from 'events';
/*
These mocks are PURPOSEFULLY minimal. A few reasons as to why:
1. I'm l̶a̶z̶y̶ a busy person :)
2. The less we have in here, the less we'll need to fix if an electron API changes
3. Only mocking what we need as we need it helps reveal areas under test where electron
is being accessed in previously unaccounted for ways
4. These mocks will get fleshed out as more unit tests are added, so if you need
something here as you are adding unit tests, then feel free to add exactly what you
need (and no more than that please).
As well, please resist the urge to turn this into a reimplimentation of electron.
When adding functions/classes, keep your implementation to only the minimal amount of code
it takes for TypeScript to recognize what you are doing. For anything more complex (including
implementation code and return values) please do that within your tests via jest with
mockImplementation or mockReturnValue.
*/
class MockBrowserWindow extends EventEmitter {
webContents: MockWebContents;
constructor(options?: unknown) {
// @ts-expect-error options is really EventEmitterOptions, but events.d.ts doesn't expose it...
super(options);
this.webContents = new MockWebContents();
}
addTabbedWindow(tab: MockBrowserWindow): void {
return;
}
focus(): void {
return;
}
static fromWebContents(webContents: MockWebContents): MockBrowserWindow {
return new MockBrowserWindow();
}
static getFocusedWindow(window: MockBrowserWindow): MockBrowserWindow {
return window ?? new MockBrowserWindow();
}
isSimpleFullScreen(): boolean {
throw new Error('Not implemented');
}
isFullScreen(): boolean {
throw new Error('Not implemented');
}
isFullScreenable(): boolean {
throw new Error('Not implemented');
}
loadURL(url: string, options?: unknown): Promise<void> {
return Promise.resolve(undefined);
}
setFullScreen(flag: boolean): void {
return;
}
setSimpleFullScreen(flag: boolean): void {
return;
}
}
class MockDialog {
static showMessageBox(
browserWindow: MockBrowserWindow,
options: unknown,
): Promise<number> {
throw new Error('Not implemented');
}
static showMessageBoxSync(
browserWindow: MockBrowserWindow,
options: unknown,
): number {
throw new Error('Not implemented');
}
}
class MockSession extends EventEmitter {
webRequest: MockWebRequest;
constructor() {
super();
this.webRequest = new MockWebRequest();
}
clearCache(): Promise<void> {
return Promise.resolve();
}
clearStorageData(): Promise<void> {
return Promise.resolve();
}
}
class MockWebContents extends EventEmitter {
session: MockSession;
constructor() {
super();
this.session = new MockSession();
}
getURL(): string {
throw new Error('Not implemented');
}
insertCSS(css: string, options?: unknown): Promise<string> {
throw new Error('Not implemented');
}
}
class MockWebRequest {
emitter: InternalEmitter;
constructor() {
this.emitter = new InternalEmitter();
}
onResponseStarted(
filter: unknown,
listener: ((details: unknown) => void) | null,
): void {
if (listener) {
this.emitter.addListener('onResponseStarted', (details: unknown) =>
listener(details),
);
}
}
send(event: string, ...args: unknown[]): void {
this.emitter.emit(event, ...args);
}
}
class InternalEmitter extends EventEmitter {}
const mockShell = {
openExternal(url: string, options?: unknown): Promise<void> {
return new Promise((resolve) => resolve());
},
};
export {
MockDialog as dialog,
MockBrowserWindow as BrowserWindow,
MockSession as Session,
MockWebContents as WebContents,
MockWebRequest as WebRequest,
mockShell as shell,
};

View File

@ -8,11 +8,27 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { OutputOptions } from '../../shared/src/options/model';
// Do *NOT* add 3rd-party imports here in preload (except for webpack `externals` like electron).
// They will work during development, but break in the prod build :-/ .
// Electron doc isn't explicit about that, so maybe *we*'re doing something wrong.
// At any rate, that's what we have now. If you want an import here, go ahead, but
// verify that apps built with a non-devbuild nativefier (installed from tarball) work.
// Recipe to monkey around this, assuming you git-cloned nativefier in /opt/nativefier/ :
// cd /opt/nativefier/ && rm -f nativefier-43.1.0.tgz && npm run build && npm pack && mkdir -p ~/n4310/ && cd ~/n4310/ \
// && rm -rf ./* && npm i /opt/nativefier/nativefier-43.1.0.tgz && ./node_modules/.bin/nativefier 'google.com'
// See https://github.com/nativefier/nativefier/issues/1175
// and https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions / preload
const log = console; // since we can't have `loglevel` here in preload
export const INJECT_DIR = path.join(__dirname, '..', 'inject');
const INJECT_JS_PATH = path.join(__dirname, '..', 'inject/inject.js');
/** /**
* Patches window.Notification to: * Patches window.Notification to:
* - set a callback on a new Notification * - set a callback on a new Notification
@ -20,9 +36,18 @@ const INJECT_JS_PATH = path.join(__dirname, '..', 'inject/inject.js');
* @param createCallback * @param createCallback
* @param clickCallback * @param clickCallback
*/ */
function setNotificationCallback(createCallback, clickCallback) { function setNotificationCallback(
createCallback: {
(title: string, opt: NotificationOptions): void;
(...args: unknown[]): void;
},
clickCallback: { (): void; (this: Notification, ev: Event): unknown },
): void {
const OldNotify = window.Notification; const OldNotify = window.Notification;
const newNotify = function (title, opt) { const newNotify = function (
title: string,
opt: NotificationOptions,
): Notification {
createCallback(title, opt); createCallback(title, opt);
const instance = new OldNotify(title, opt); const instance = new OldNotify(title, opt);
instance.addEventListener('click', clickCallback); instance.addEventListener('click', clickCallback);
@ -33,33 +58,295 @@ function setNotificationCallback(createCallback, clickCallback) {
get: () => OldNotify.permission, get: () => OldNotify.permission,
}); });
// @ts-ignore // @ts-expect-error TypeScript says its not compatible, but it works?
window.Notification = newNotify; window.Notification = newNotify;
} }
function injectScripts() { async function getDisplayMedia(
const needToInject = fs.existsSync(INJECT_JS_PATH); sourceId: number | string,
): Promise<MediaStream> {
type OriginalVideoPropertyType = boolean | MediaTrackConstraints | undefined;
if (!window?.navigator?.mediaDevices) {
throw Error('window.navigator.mediaDevices is not present');
}
// Electron supports an outdated specification for mediaDevices,
// see https://www.electronjs.org/docs/latest/api/desktop-capturer/
const stream = await window.navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
},
} as unknown as OriginalVideoPropertyType,
});
return stream;
}
function setupScreenSharePickerStyles(id: string): void {
const screenShareStyles = document.createElement('style');
screenShareStyles.id = id;
screenShareStyles.innerHTML = `
.desktop-capturer-selection {
--overlay-color: hsla(0, 0%, 11.8%, 0.75);
--highlight-color: highlight;
--text-content-color: #fff;
--selection-button-color: hsl(180, 1.3%, 14.7%);
}
.desktop-capturer-selection {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: var(--overlay-color);
color: var(--text-content-color);
z-index: 10000000;
display: flex;
align-items: center;
justify-content: center;
}
.desktop-capturer-selection__close {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
padding: 1rem;
color: inherit;
position: absolute;
left: 1rem;
top: 1rem;
cursor: pointer;
}
.desktop-capturer-selection__scroller {
width: 100%;
max-height: 100vh;
overflow-y: auto;
}
.desktop-capturer-selection__list {
max-width: calc(100% - 100px);
margin: 50px;
padding: 0;
display: flex;
flex-wrap: wrap;
list-style: none;
overflow: hidden;
justify-content: center;
}
.desktop-capturer-selection__item {
display: flex;
margin: 4px;
}
.desktop-capturer-selection__btn {
display: flex;
flex-direction: column;
align-items: stretch;
width: 145px;
margin: 0;
border: 0;
border-radius: 3px;
padding: 4px;
background: var(--selection-button-color);
text-align: left;
transition: background-color .15s, box-shadow .15s;
}
.desktop-capturer-selection__btn:hover,
.desktop-capturer-selection__btn:focus {
background: var(--highlight-color);
}
.desktop-capturer-selection__thumbnail {
width: 100%;
height: 81px;
object-fit: cover;
}
.desktop-capturer-selection__name {
margin: 6px 0 6px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
@media (prefers-color-scheme: light) {
.desktop-capturer-selection {
--overlay-color: hsla(0, 0%, 90.2%, 0.75);
--text-content-color: hsl(0, 0%, 12.9%);
--selection-button-color: hsl(180, 1.3%, 85.3%);
}
}`;
document.head.appendChild(screenShareStyles);
}
function setupScreenSharePickerElement(
id: string,
sources: Electron.DesktopCapturerSource[],
): void {
const selectionElem = document.createElement('div');
selectionElem.classList.add('desktop-capturer-selection');
selectionElem.id = id;
selectionElem.innerHTML = `
<button class="desktop-capturer-selection__close" id="${id}-close" aria-label="Close screen share picker" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32">
<path fill="currentColor" d="m12 10.586 4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/>
</svg>
</button>
<div class="desktop-capturer-selection__scroller">
<ul class="desktop-capturer-selection__list">
${sources
.map(
({ id, name, thumbnail }) => `
<li class="desktop-capturer-selection__item">
<button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}">
<img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" />
<span class="desktop-capturer-selection__name">${name}</span>
</button>
</li>
`,
)
.join('')}
</ul>
</div>
`;
document.body.appendChild(selectionElem);
}
function setupScreenSharePicker(
resolve: (value: MediaStream | PromiseLike<MediaStream>) => void,
reject: (reason?: unknown) => void,
sources: Electron.DesktopCapturerSource[],
): void {
const baseElementsId = 'native-screen-share-picker';
const pickerStylesElementId = baseElementsId + '-styles';
setupScreenSharePickerElement(baseElementsId, sources);
setupScreenSharePickerStyles(pickerStylesElementId);
const clearElements = (): void => {
document.getElementById(pickerStylesElementId)?.remove();
document.getElementById(baseElementsId)?.remove();
};
document
.getElementById(`${baseElementsId}-close`)
?.addEventListener('click', () => {
clearElements();
reject('Screen share was cancelled by the user.');
});
document
.querySelectorAll('.desktop-capturer-selection__btn')
.forEach((button) => {
button.addEventListener('click', () => {
const id = button.getAttribute('data-id');
if (!id) {
log.error("Couldn't find `data-id` of element");
clearElements();
return;
}
const source = sources.find((source) => source.id === id);
if (!source) {
log.error(`Source with id "${id}" does not exist`);
clearElements();
return;
}
getDisplayMedia(source.id)
.then((stream) => {
resolve(stream);
})
.catch((err) => {
log.error('Error selecting desktop capture source:', err);
reject(err);
})
.finally(() => {
clearElements();
});
});
});
}
function setDisplayMediaPromise(): void {
// Since no implementation for `getDisplayMedia` exists in Electron we write our own.
if (!window?.navigator?.mediaDevices) {
return;
}
window.navigator.mediaDevices.getDisplayMedia = (): Promise<MediaStream> => {
return new Promise((resolve, reject) => {
const sources = ipcRenderer.invoke(
'desktop-capturer-get-sources',
) as Promise<Electron.DesktopCapturerSource[]>;
sources
.then(async (sources) => {
if (isWayland()) {
// No documentation is provided wether the first element is always PipeWire-picked or not
// i.e. maybe it's not deterministic, we are only taking a guess here.
const stream = await getDisplayMedia(sources[0].id);
resolve(stream);
} else {
setupScreenSharePicker(resolve, reject, sources);
}
})
.catch((err) => {
reject(err);
});
});
};
}
function injectScripts(): void {
const needToInject = fs.existsSync(INJECT_DIR);
if (!needToInject) { if (!needToInject) {
return; return;
} }
// Dynamically require scripts // Dynamically require scripts
require(INJECT_JS_PATH); try {
const jsFiles = fs
.readdirSync(INJECT_DIR, { withFileTypes: true })
.filter(
(injectFile) => injectFile.isFile() && injectFile.name.endsWith('.js'),
)
.map((jsFileStat) => path.join('..', 'inject', jsFileStat.name));
for (const jsFile of jsFiles) {
log.debug('Injecting JS file', jsFile);
require(jsFile);
}
} catch (err: unknown) {
log.error('Error encoutered injecting JS files', err);
}
} }
function notifyNotificationCreate(title, opt) { function notifyNotificationCreate(
title: string,
opt: NotificationOptions,
): void {
ipcRenderer.send('notification', title, opt); ipcRenderer.send('notification', title, opt);
} }
function notifyNotificationClick() { function notifyNotificationClick(): void {
ipcRenderer.send('notification-click'); ipcRenderer.send('notification-click');
} }
// @ts-expect-error TypeScript thinks these are incompatible but they aren't
setNotificationCallback(notifyNotificationCreate, notifyNotificationClick); setNotificationCallback(notifyNotificationCreate, notifyNotificationClick);
setDisplayMediaPromise();
ipcRenderer.on('params', (event, message) => { ipcRenderer.on('params', (event, message: string) => {
const appArgs = JSON.parse(message); log.debug('ipcRenderer.params', { event, message });
console.info('nativefier.json', appArgs); const appArgs: unknown = JSON.parse(message) as OutputOptions;
log.info('nativefier.json', appArgs);
}); });
ipcRenderer.on('debug', (event, message) => { ipcRenderer.on('debug', (event, message: string) => {
console.info('debug:', message); log.debug('ipcRenderer.debug', { event, message });
}); });
// Copy-pastaed as unable to get imports to work in preload.
// If modifying, update also app/src/helpers/helpers.ts
function isWayland(): boolean {
return (
isLinux() &&
(Boolean(process.env.WAYLAND_DISPLAY) ||
process.env.XDG_SESSION_TYPE === 'wayland')
);
}
function isLinux(): boolean {
return os.platform() === 'linux';
}

View File

@ -1,19 +1,36 @@
{ {
"extends": "../tsconfig-base.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true,
"declaration": false,
"esModuleInterop": true,
"incremental": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist", "outDir": "./dist",
"resolveJsonModule": true, // Here in app/tsconfig.json, we want to set the `target` and `lib` keys to
"skipLibCheck": true, // the "best" values for the version of Node **coming with the chosen Electron**.
"sourceMap": true, // Careful: we're *not* talking about Nativefier's (CLI) required Node version,
"target": "es2017", // we're talking about the version of the Node runtime **bundled with Electron**.
"lib": ["es2017", "dom"] //
// Like in our main tsconfig.json, we want to be as conservative as possible,
// to support (as much as reasonable) users using old versions of Electron.
// Then, at some point, an app dependency (declared in app/package.json)
// will require a more recent Node, then it's okay to bump our app compilerOptions
// to what's supported by the more recent Node.
//
// TS doesn't offer any easy "preset" for this, so the best we have is to
// believe people who know which {syntax, library} parts of current EcmaScript
// are supported for the version of Node coming with the Electron being used,
// and use what they recommend. For the current Node version, I followed
// https://stackoverflow.com/questions/51716406/typescript-tsconfig-settings-for-node-js-10
// and 'dom' to tell tsc it's okay to use the URL object (which is in Node >= 7)
"target": "es2018",
"lib": [
"es2018",
"dom"
]
}, },
"include": [ "include": [
"./src/**/*" "./src/**/*"
],
"references": [
{
"path": "../shared"
}
] ]
} }

View File

@ -1,5 +1,7 @@
const path = require('path'); const path = require('path');
// Q: Why do you use webpack?
// A: https://github.com/nativefier/nativefier/commit/cde5c1e13bdc2739604cab04bac64eae0d719ed1
module.exports = { module.exports = {
target: 'node', target: 'node',
entry: './src/main.ts', entry: './src/main.ts',

39
base-eslintrc.js Normal file
View File

@ -0,0 +1,39 @@
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
module.exports = {
parser: '@typescript-eslint/parser',
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: {
'no-console': 'error',
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-invalid-void-type': 'error',
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/typedef': 'error',
'@typescript-eslint/unified-signatures': 'error',
},
// https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files
ignorePatterns: [
'node_modules/**',
'app/node_modules/**',
'app/lib/**',
'lib/**',
'built-tests/**',
'coverage/**',
],
};

View File

@ -1,894 +0,0 @@
# API
## Table of Contents
- [Table of Contents](#table-of-contents)
- [Packaging Squirrel-based installers](#packaging-squirrel-based-installers)
- [Command Line](#command-line)
- [Target Url](#target-url)
- [[dest]](#dest)
- [Help](#help)
- [Version](#version)
- [[name]](#name)
- [[platform]](#platform)
- [[arch]](#arch)
- [[app-copyright]](#app-copyright)
- [[app-version]](#app-version)
- [[build-version]](#build-version)
- [[electron-version]](#electron-version)
- [[widevine]](#widevine)
- [[no-overwrite]](#no-overwrite)
- [[conceal]](#conceal)
- [[icon]](#icon)
- [Packaging for Windows](#packaging-for-windows)
- [Packaging for Linux](#packaging-for-linux)
- [Packaging for macOS](#packaging-for-macos)
- [Manually Converting `.icns`](#manually-converting-icns)
- [[counter]](#counter)
- [[bounce]](#bounce)
- [[width]](#width)
- [[height]](#height)
- [[min-width]](#min-width)
- [[min-height]](#min-height)
- [[max-width]](#max-width)
- [[max-height]](#max-height)
- [[x]](#x)
- [[y]](#y)
- [[show-menu-bar]](#show-menu-bar)
- [[fast-quit]](#fast-quit)
- [[user-agent]](#user-agent)
- [[honest]](#honest)
- [[ignore-certificate]](#ignore-certificate)
- [[disable-gpu]](#disable-gpu)
- [[ignore-gpu-blacklist]](#ignore-gpu-blacklist)
- [[enable-es3-apis]](#enable-es3-apis)
- [[insecure]](#insecure)
- [[internal-urls]](#internal-urls)
- [[block-external-urls]](#block-external-urls)
- [[proxy-rules]](#proxy-rules)
- [[flash]](#flash)
- [[flash-path]](#flash-path)
- [[disk-cache-size]](#disk-cache-size)
- [[inject]](#inject)
- [[full-screen]](#full-screen)
- [[maximize]](#maximize)
- [[hide-window-frame]](#hide-window-frame)
- [[title-bar-style]](#title-bar-style)
- [[verbose]](#verbose)
- [[disable-context-menu]](#disable-context-menu)
- [[disable-dev-tools]](#disable-dev-tools)
- [[crash-reporter]](#crash-reporter)
- [[zoom]](#zoom)
- [[single-instance]](#single-instance)
- [[clear-cache]](#clear-cache)
- [[tray]](#tray)
- [[basic-auth-username]](#basic-auth-username)
- [[processEnvs]](#processenvs)
- [[file-download-options]](#file-download-options)
- [[always-on-top]](#always-on-top)
- [[global-shortcuts]](#global-shortcuts)
- [[browserwindow-options]](#browserwindow-options)
- [[darwin-dark-mode-support]](#darwin-dark-mode-support)
- [[background-color]](#background-color)
- [[disable-old-build-warning-yesiknowitisinsecure]](#disable-old-build-warning-yesiknowitisinsecure)
- [Programmatic API](#programmatic-api)
- [Addition packaging options for Windows](#addition-packaging-options-for-windows)
- [[version-string]](#version-string)
- [[win32metadata]](#win32metadata)
- [Programmatic API](#programmatic-api)
## Packaging Squirrel-based installers
See [PR #744 - Support packaging nativefier applications into Squirrel-based installers](https://github.com/jiahaog/nativefier/pull/744)
## Command Line
```bash
nativefier [options] <targetUrl> [dest]
```
Command line options are listed below.
#### Target Url
The url to point the application at.
#### [dest]
Specifies the destination directory to build the app to, defaults to the current working directory.
#### Help
```
-h, --help
```
Prints the usage information.
#### Version
```
-V, --version
```
Prints the version of your `nativefier` install.
#### [name]
```
-n, --name <value>
```
The name of the application, which will affect strings in titles and the icon.
**For Linux Users:** Do not put spaces if you define the app name yourself with `--name`, as this will cause problems (tested on Ubuntu 14.04) when pinning a packaged app to the launcher.
#### [platform]
```
-p, --platform <value>
```
Automatically determined based on the current OS. Can be overwritten by specifying either `linux`, `windows`, `osx` or `mas` for a Mac App Store specific build.
The alternative values `win32` (for Windows) or `darwin`, `mac` (for macOS) can also be used.
#### [arch]
```
-a, --arch <value>
```
The processor architecture to target when building.
- Automatically set to the build-time machine architecture...
- ... or can be overridden by specifying one of: `x64`, `arm`, `arm64`, `ia32`.
#### [app-copyright]
```
--app-copyright <value>
```
The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata property on Windows, and `NSHumanReadableCopyright` on OS X.
#### [app-version]
```
--app-version <value>
```
The release version of the application. By default the `version` property in the `package.json` is used but it can be overridden with this argument. If neither are provided, the version of Electron will be used. Maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on OS X.
#### [build-version]
```
--build-version <value>
```
The build version of the application. Maps to the `FileVersion` metadata property on Windows, and `CFBundleVersion` on OS X.
#### [electron-version]
```
-e, --electron-version <value>
```
Electron version without the `v`, see https://github.com/atom/electron/releases.
#### [widevine]
```
--widevine
```
Use a Widevine-enabled version of Electron for DRM playback, see https://github.com/castlabs/electron-releases.
#### [no-overwrite]
```
--no-overwrite
```
Specifies if the destination directory should be not overwritten, defaults to false.
#### [conceal]
```
-c, --conceal
```
Specifies if the source code within the nativefied app should be packaged into an archive, defaults to false, [read more](http://electron.atom.io/docs/v0.36.0/tutorial/application-packaging/).
#### [icon]
```
-i, --icon <path>
```
##### Packaging for Windows
The icon parameter should be a path to a `.ico` file.
##### Packaging for Linux
The icon parameter should be a path to a `.png` file.
##### Packaging for macOS
The icon parameter can either be a `.icns` or a `.png` file if the [optional dependencies](../README.md#optional-dependencies) are installed.
If you have the optional dependencies `iconutil`, Imagemagick `convert`, and Imagemagick `identify` in your `PATH`, Nativefier will automatically convert the `.png` to a `.icns` for you.
###### Manually Converting `.icns`
[iConvertIcons](https://iconverticons.com/online/) can be used to convert `.pngs`, though it can be quite cumbersome.
To retrieve the `.icns` file from the downloaded file, extract it first and press File > Get Info. Then select the icon in the top left corner of the info window and press `⌘-C`. Open Preview and press File > New from clipboard and save the `.icns` file. It took me a while to figure out how to do that and question why a `.icns` file was not simply provided in the downloaded archive.
#### [counter]
```
--counter
```
Use a counter that persists even with window focus for the application badge for sites that use an "(X)" format counter in the page title (i.e. Gmail).
#### [bounce]
```
--bounce
```
(macOS only) When the counter increases, the dock icon will bounce for one second. This only works if the `--counter` option is active.
#### [width]
```
--width <value>
```
Width of the packaged application, defaults to `1280px`.
#### [height]
```
--height <value>
```
Height of the packaged application, defaults to `800px`.
#### [min-width]
```
--min-width <value>
```
Minimum width of the packaged application, defaults to `0`.
#### [min-height]
```
--min-height <value>
```
Minimum height of the packaged application, defaults to `0`.
#### [max-width]
```
--max-width <value>
```
Maximum width of the packaged application, default is no limit.
#### [max-height]
```
--max-height <value>
```
Maximum height of the packaged application, default is no limit.
#### [x]
```
--x <value>
```
X location of the packaged application window.
#### [y]
```
--y <value>
```
Y location of the packaged application window.
#### [show-menu-bar]
```
-m, --show-menu-bar
```
Specifies if the menu bar should be shown.
#### [fast-quit]
```
-f, --fast-quit
```
(macOS only) Specifies to quit the app after closing all windows, defaults to false.
#### [user-agent]
```
-u, --user-agent <value>
```
Set the user agent to run the created app with.
#### [honest]
```
--honest
```
By default, Nativefier uses a preset user agent string for your OS and masquerades as a regular Google Chrome browser, so that sites like WhatsApp Web will not say that the current browser is unsupported.
If this flag is passed, it will not override the user agent.
#### [ignore-certificate]
```
--ignore-certificate
```
Forces the packaged app to ignore certificate errors.
#### [disable-gpu]
```
--disable-gpu
```
Disable hardware acceleration for the packaged application.
#### [ignore-gpu-blacklist]
```
--ignore-gpu-blacklist
```
Passes the ignore-gpu-blacklist flag to the Chrome engine, to allow for WebGl apps to work on non supported graphics cards.
#### [enable-es3-apis]
```
--enable-es3-apis
```
Passes the enable-es3-apis flag to the Chrome engine, to force the activation of WebGl 2.0.
#### [insecure]
```
--insecure
```
Forces the packaged app to ignore web security errors, such as [Mixed Content](https://developer.mozilla.org/en-US/docs/Security/Mixed_content) errors when receiving HTTP content on a HTTPS site.
#### [internal-urls]
```
--internal-urls <regex>
```
Regular expression of URLs to consider "internal"; all other URLs will be opened in an external browser. Defaults to URLs on same second-level domain as app.
Example:
```bash
nativefier https://google.com --internal-urls ".*?\.google\.*?"
```
Or, if you want to allow all domains for example for external auths,
```bash
nativefier https://google.com --internal-urls ".*?"
```
#### [block-external-urls]
```
--block-external-urls
```
Forbid navigation to URLs not considered "internal" (see '--internal-urls'). Instead of opening in an external browser, attempts to navigate to external URLs will be blocked, and an error message will be shown. Default: false
Example:
```bash
nativefier https://google.com --internal-urls ".*?\.google\.*?" --block-external-urls
```
Blocks navigation to any URLs except Google and its subdomains.
#### [proxy-rules]
```
--proxy-rules <value>
```
Proxy rules. See [proxyRules](https://electronjs.org/docs/api/session?q=proxy#sessetproxyconfig-callback) for more details.
Example:
```bash
nativefier https://google.com --proxy-rules http://127.0.0.1:1080
```
#### [flash]
```
--flash
```
If `--flash` is specified, Nativefier will automatically try to determine the location of your Google Chrome flash binary. Take note that the version of Chrome on your computer should be the same as the version used by the version of Electron for the Nativefied package.
Take note that if this flag is specified, the `--insecure` flag will be added automatically, to prevent the Mixed Content errors on sites such as [Twitch.tv](https://www.twitch.tv/).
#### [flash-path]
```
--flash-path <value>
```
You can also specify the path to the Chrome flash plugin directly with this flag. The path can be found at [chrome://plugins](chrome://plugins), under `Adobe Flash Player` > `Location`. This flag automatically enables the `--flash` flag as well.
#### [disk-cache-size]
```
--disk-cache-size <value>
```
Forces the maximum disk space to be used by the disk cache. Value is given in bytes.
#### [inject]
```
--inject <value>
```
Allows you to inject a javascript or css file. This command can be run multiple times to inject the files.
_Note:_ The javascript file is loaded _after_ `DOMContentLoaded`, so you can assume the DOM is complete & available.
Example:
```bash
nativefier http://google.com --inject ./some-js-injection.js --inject ./some-css-injection.css ~/Desktop
```
#### [full-screen]
```
--full-screen
```
Makes the packaged app start in full screen.
#### [maximize]
```
--maximize
```
Makes the packaged app start maximized.
#### [hide-window-frame]
```
--hide-window-frame
```
Disable window frame and controls.
#### [title-bar-style]
```
--title-bar-style <value>
```
(macOS only) Sets the style for the app's title bar. See more details at electron's [Frameless Window](https://github.com/electron/electron/blob/master/docs/api/frameless-window.md#alternatives-on-macos) documentation.
Consider injecting a custom CSS (via `--inject`) for better integration. Specifically, the CSS should specify a draggable region. For instance, if the target website has a `<header>` element, you can make it draggable like so.
```css
/* site.css */
/* header is draggable... */
header {
-webkit-app-region: drag;
}
/* but any buttons inside the header shouldn't be draggable */
header button {
-webkit-app-region: no-drag;
}
/* perhaps move some items out of way for the traffic light */
header div:first-child {
margin-left: 100px;
margin-top: 25px;
}
```
```sh
nativefier http://google.com --inject site.css --title-bar-style 'hiddenInset'
```
#### [verbose]
```
--verbose
```
Shows detailed logs in the console.
#### [disable-context-menu]
```
--disable-context-menu
```
Disable the context menu
#### [disable-dev-tools]
```
--disable-dev-tools
```
Disable the Chrome developer tools
#### [crash-reporter]
```
--crash-reporter <value>
```
Enables crash reporting and set the URL to submit crash reports to
Example:
```bash
nativefier http://google.com --crash-reporter https://electron-crash-reporter.appspot.com/PROJECT_ID/create/
```
#### [zoom]
```
--zoom <value>
```
Sets a default zoom factor to be used when the app is opened, defaults to `1.0`.
#### [single-instance]
```
--single-instance
```
Prevents application from being run multiple times. If such an attempt occurs the already running instance is brought to front.
#### [clear-cache]
```
--clear-cache
```
Prevents the application from preserving cache between launches.
#### [tray]
```
--tray [start-in-tray]
```
Application will stay as an icon in the system tray. Prevents application from being closed from clicking the window close button.
When the optional argument `start-in-tray` is provided, i.e. the application is started using `--tray start-in-tray`, the main window will not be shown on first start.
#### [basic-auth-username]
```
--basic-auth-username <value> --basic-auth-password <value>
```
Set basic http(s) auth via the command line to have the app automatically log you in to a protected site. Both fields are required if one is set.
#### [processEnvs]
```
--processEnvs <json-string>
```
a JSON string of key/value pairs to be set as environment variables before any browser windows are opened.
Example:
```bash
nativefier <your-geolocation-enabled-website> --processEnvs '{"GOOGLE_API_KEY": "<your-google-api-key>"}'
```
#### [file-download-options]
```
--file-download-options <json-string>
```
a JSON string of key/value pairs to be set as file download options. See [electron-dl](https://github.com/sindresorhus/electron-dl) for available options.
Example:
```bash
nativefier <your-website> --file-download-options '{"saveAs": true}'
```
#### [always-on-top]
```
--always-on-top
```
Enable always on top for the packaged application.
#### [global-shortcuts]
```
--global-shortcuts shortcuts.json
```
Register global shortcuts which will trigger input events like key presses or pointer events in the application.
You may define multiple global shortcuts which can trigger a series of input events. It has the following structure:
```js
[
{
// Key is passed as first argument to globalShortcut.register
key: 'CommandOrControl+Shift+Z',
// The input events exactly match the event config in Electron for contents.sendInputEvent(event)
inputEvents: [
{
// Available event types: mouseDown, mouseUp, mouseEnter, mouseLeave, contextMenu, mouseWheel, mouseMove, keyDown, keyUp or char
type: 'keyDown',
// Further config depends on your event type. See docs at: https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentssendinputeventevent
keyCode: 'Space',
},
],
},
];
```
**Important note for using modifier keys:**
If you want to trigger key events which include a modifier (Ctrl, Shift,...), you need to keyDown the modifier key first, then keyDown the actual key _including_ the modifier key as modifier property and then keyUp both keys again. No idea what this means? See the example for `MediaPreviousTrack` below!
**For more details, please see the Electron documentation:**
- List of available keys: https://github.com/electron/electron/blob/master/docs/api/accelerator.md
- Details about how to create input event objects: https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentssendinputeventevent
Example `shortcuts.json` for `https://deezer.com` & `https://soundcloud.com` to get your play/pause/previous/next media keys working:
```json
[
{
"key": "MediaPlayPause",
"inputEvents": [
{
"type": "keyDown",
"keyCode": "Space"
}
]
},
{
"key": "MediaPreviousTrack",
"inputEvents": [
{
"type": "keyDown",
"keyCode": "Shift"
},
{
"type": "keyDown",
"keyCode": "Left",
"modifiers": ["shift"]
},
{
"type": "keyUp",
"keyCode": "Left",
"modifiers": ["shift"]
},
{
"type": "keyUp",
"keyCode": "Shift"
}
]
},
{
"key": "MediaNextTrack",
"inputEvents": [
{
"type": "keyDown",
"keyCode": "Shift"
},
{
"type": "keyDown",
"keyCode": "Right",
"modifiers": ["shift"]
},
{
"type": "keyUp",
"keyCode": "Right",
"modifiers": ["shift"]
},
{
"type": "keyUp",
"keyCode": "Shift"
}
]
}
]
```
#### [browserwindow-options]
```
--browserwindow-options <json-string>
```
a JSON string that will be sent directly into electron BrowserWindow options.
See [Electron's BrowserWindow API Documentation](https://electronjs.org/docs/api/browser-window#new-browserwindowoptions) for the complete list of options.
Example:
```bash
nativefier <your-website> --browserwindow-options '{ "webPreferences": { "defaultFontFamily": { "standard": "Comic Sans MS", "serif": "Comic Sans MS" } } }'
```
#### [darwin-dark-mode-support]
```
--darwin-dark-mode-support
```
Enables Dark Mode support on macOS 10.4+.
#### [background-color]
```
--background-color <string>
```
See https://electronjs.org/docs/api/browser-window#setting-backgroundcolor
## Programmatic API
You can use the Nativefier programmatic API as well.
```bash
# install and save to package.json
npm install --save nativefier
```
In your `.js` file:
```javascript
var nativefier = require('nativefier').default;
// possible options, defaults unless specified otherwise
var options = {
name: 'Web WhatsApp', // will be inferred if not specified
targetUrl: 'http://web.whatsapp.com', // required
platform: 'darwin', // defaults to the current system
arch: 'x64', // defaults to the current system
version: '0.36.4',
out: '.',
overwrite: false,
asar: false, // see conceal
icon: '~/Desktop/icon.png',
counter: false,
bounce: false,
width: 1280,
height: 800,
showMenuBar: false,
fastQuit: false,
userAgent: 'Mozilla ...', // will infer a default for your current system
ignoreCertificate: false,
ignoreGpuBlacklist: false,
enableEs3Apis: false,
internalUrls: '.*?', // defaults to URLs on same second-level domain as app
blockExternalUrls: false,
insecure: false,
honest: false,
zoom: 1.0,
singleInstance: false,
clearCache: false,
fileDownloadOptions: {
saveAs: true, // always show "Save As" dialog
},
processEnvs: {
GOOGLE_API_KEY: '<your-google-api-key>',
},
};
nativefier(options, function (error, appPath) {
if (error) {
console.error(error);
return;
}
console.log('App has been nativefied to', appPath);
});
```
### Addition packaging options for Windows
#### [version-string]
_Object_ (**deprecated** as removed in `electron-packager` 9.0.0, please use the
[`win32metadata`](#win32metadata) parameter instead)
#### [win32metadata]
```
--win32metadata <json-string>
```
a JSON string of key/value pairs of application metadata (ProductName, InternalName, FileDescription) to embed into the executable (Windows only).
Example:
```bash
nativefier <your-geolocation-enabled-website> --win32metadata '{"ProductName": "Your Product Name", "InternalName", "Your Internal Name", "FileDescription": "Your File Description"}'
```
#### [disable-old-build-warning-yesiknowitisinsecure]
Disables the warning shown when opening a Nativefier app made a long time ago, using an old and probably insecure Electron. Nativefier uses the Chrome browser (through Electron), and remaining on an old version is A. performance sub-optimal and B. dangerous.
However, there are legitimate use cases to disable such a warning. For example, if you are using Nativefier to ship a kiosk app exposing an internal site (over which you have control). Under those circumstances, it is reasonable to disable this warning that you definitely don't want end-users to see.
##### Programmatic API
_Object_
Object (also known as a "hash") of application metadata to embed into the executable:
- `CompanyName`
- `FileDescription`
- `OriginalFilename`
- `ProductName`
- `InternalName`
_(Note that `win32metadata` was added to `electron-packager` in version 8.0.0)_
In your `.js` file:
```javascript
var options = {
...
win32metadata: {
CompanyName: 'Your Company Name',
FileDescription: 'Your File Description',
OriginalFilename: 'Your Original Filename',
ProductName: 'Your Product Name',
InternalName: 'Your Internal Name'
}
};
```
More description about the options for `nativefier` can be found at the above [section](#command-line).

View File

@ -1,73 +0,0 @@
# Development Guide
## Setup
First, clone the project
```bash
git clone https://github.com/jiahaog/nativefier.git
cd nativefier
```
Install dependencies for both the CLI and the Electron app:
```bash
# Under Linux and macOS:
npm run dev-up
# Under Windows:
npm run dev-up-win
```
Build nativefier:
```bash
npm run build
```
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, you can run Nativefier with your test parameters:
```bash
nativefier --your-awesome-new-flag 'https://your-test-site.com'
```
Then run your nativefier app _through the command line too_ (to see logs & errors):
```bash
# 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
- 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:
1. Run a TSC watcher: `npm run build:watch`
2. Run a Jest unit tests watcher: `npm run test:watch`
- Alternatively, you can run both test processes in the same terminal by running: `npm run watch`

View File

@ -1,51 +0,0 @@
#!/usr/bin/env bash
#
# Updates the changelog and version in the package.json
# Will also create a commit with these changes locally
# Run `git commit --amend` after that if you wish to make changes
#
# Usage:
# ./changelog "7.0.0"
#
# Prerequisites:
# - On master branch
# - No uncommitted changes
#
# Dependencies:
# - git-extras https://github.com/tj/git-extras/blob/master/Installation.md
set -eo pipefail
# Checks if we are on the master branch
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$BRANCH" != 'master' ]]; then
echo 'ERROR: not on master branch' >&2
exit 1;
fi
# Checks if there are uncommitted changes
git diff-index --quiet HEAD -- || (echo 'ERROR: there are uncommitted changes' >&2 && exit 1)
VERSION="$1"
# Validates the $VERSION
SEMVER_REGEX='^([0-9]+\.){2}([0-9]+)$'
if ! [[ $VERSION =~ $SEMVER_REGEX ]]; then
echo "ERROR: Version '$VERSION' is invalid " >&2
exit 1
fi
# Change the version in the package.json
cat package.json | jq ".version = \"$VERSION\"" > package.json.tmp
# Workaround for inplace jq editing
mv package.json.tmp package.json
# Unset the editor so that git changelog does not open a editor
EDITOR=:
git changelog CHANGELOG.md --tag "$VERSION"
# Commit these changes
git add CHANGELOG.md
git add package.json
git commit -m "Update changelog for \`v$VERSION\`"

View File

@ -1,61 +0,0 @@
#!/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

View File

@ -1,51 +0,0 @@
# Release
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.
## Tests
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
# With no 'v'. For example: npm run changelog -- 7.7.1
```
This command does 3 things:
1. Update the version in the `package.json`
2. Update the changelog
3. Creates a new commit with the changes
Now we may want to cleanup the changelog:
```bash
vim CHANGELOG.md
git commit --amend
```
Once we are satisfied,
```bash
git tag -a vX.Y.Z -m 'vX.Y.Z'
git push --follow-tags origin master
```
On [GitHub Releases](https://github.com/jiahaog/nativefier/releases),
draft and publish a new release with title `Nativefier vX.Y.Z` (yes, with a `v`).
Our CI will react on the new release, and publish it to npm.
The new version will be visible on npm within a few minutes.

View File

@ -13,13 +13,15 @@ set -e
HAVE_IMAGEMAGICK= HAVE_IMAGEMAGICK=
HAVE_ICONUTIL= HAVE_ICONUTIL=
HAVE_SIPS= HAVE_SIPS=
HAVE_GRAPHICSMAGICK=
type convert &>/dev/null && HAVE_IMAGEMAGICK=true type convert &>/dev/null && HAVE_IMAGEMAGICK=true
type iconutil &>/dev/null && HAVE_ICONUTIL=true type iconutil &>/dev/null && HAVE_ICONUTIL=true
type sips &>/dev/null && HAVE_SIPS=true type sips &>/dev/null && HAVE_SIPS=true
type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && HAVE_GRAPHICSMAGICK=true
[[ -z "$HAVE_ICONUTIL" ]] && { echo >&2 "Cannot find required iconutil executable"; exit 1; } [[ -z "$HAVE_ICONUTIL" ]] && { echo >&2 "Cannot find required iconutil executable"; exit 1; }
[[ -z "$HAVE_IMAGEMAGICK" && -z "$HAVE_SIPS" ]] && { echo >&2 "Cannot find required image converter, please install sips or imagemagick"; exit 1; } [[ -z "$HAVE_IMAGEMAGICK" && -z "$HAVE_SIPS" && -z "$HAVE_GRAPHICSMAGICK" ]] && { echo >&2 "Cannot find required image converter, please install sips, imagemagick or graphicsmagick"; exit 1; }
# Parameters # Parameters
SOURCE="$1" SOURCE="$1"
@ -36,7 +38,7 @@ if [ -z "${DEST}" ]; then
exit 1 exit 1
fi fi
TEMP_DIR="$(mktemp -d)" TEMP_DIR="$(mktemp -d -t nativefier-icns-XXXXXX)"
ICONSET="${TEMP_DIR}/converted.iconset" ICONSET="${TEMP_DIR}/converted.iconset"
function cleanUp() { function cleanUp() {

View File

@ -8,7 +8,12 @@
set -e set -e
type convert >/dev/null 2>&1 || { echo >&2 "Cannot find required ImageMagick Convert executable"; exit 1; } CONVERT=
type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && CONVERT="gm convert"
type convert &>/dev/null && CONVERT="convert"
[[ -z "$CONVERT" ]] && { echo >&2 "Cannot find required ImageMagick Convert or GraphicsMagick executable"; exit 1; }
SOURCE=$1 SOURCE=$1
DEST=$2 DEST=$2
@ -31,4 +36,4 @@ if [ "${EXT}" == "ico" ]; then
exit 0 exit 0
fi fi
convert "${SOURCE}" -resize 256x256 "${DEST}" $CONVERT "${SOURCE}" -resize 256x256 "${DEST}"

View File

@ -17,8 +17,8 @@ make_iconset_imagemagick() {
mkdir "$iconset" mkdir "$iconset"
for size in {16,32,64,128,256,512}; do for size in {16,32,64,128,256,512}; do
convert "${file}" -define png:big-depth=16 -define png:color-type=6 -sample "${size}x${size}" "${iconset}/icon_${size}x${size}.png" $CONVERT "${file}" -define png:big-depth=16 -define png:color-type=6 -sample "${size}x${size}" "${iconset}/icon_${size}x${size}.png"
convert "${file}" -define png:big-depth=16 -define png:color-type=6 -sample "$((size * 2))x$((size * 2))" "${iconset}/icon_${size}x${size}@2x.png" $CONVERT "${file}" -define png:big-depth=16 -define png:color-type=6 -sample "$((size * 2))x$((size * 2))" "${iconset}/icon_${size}x${size}@2x.png"
done done
} }
@ -50,15 +50,18 @@ fi
HAVE_IMAGEMAGICK= HAVE_IMAGEMAGICK=
HAVE_SIPS= HAVE_SIPS=
HAVE_GRAPHICSMAGICK=
CONVERT=
type convert &>/dev/null && HAVE_IMAGEMAGICK=true type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && HAVE_GRAPHICSMAGICK=true && CONVERT="gm convert"
type convert &>/dev/null && HAVE_IMAGEMAGICK=true && CONVERT="convert"
type sips &>/dev/null && HAVE_SIPS=true type sips &>/dev/null && HAVE_SIPS=true
if [[ ! -z "$HAVE_IMAGEMAGICK" ]]; then if [[ -n "$HAVE_IMAGEMAGICK" || -n "$HAVE_GRAPHICSMAGICK" ]]; then
PNG_PATH="$(mktemp -d)/icon.png" PNG_PATH="$(mktemp -d -t nativefier-iconset-XXXXXX)/icon.png"
"${BASH_SOURCE%/*}/convertToPng" "${SOURCE}" "${PNG_PATH}" "${BASH_SOURCE%/*}/convertToPng" "${SOURCE}" "${PNG_PATH}"
make_iconset_imagemagick "${PNG_PATH}" "${DEST}" make_iconset_imagemagick "${PNG_PATH}" "${DEST}"
elif [[ ! -z "$HAVE_SIPS" ]]; then elif [[ -n "$HAVE_SIPS" ]]; then
make_iconset_sips "${SOURCE}" "${DEST}" make_iconset_sips "${SOURCE}" "${DEST}"
else else
echo >&2 "Cannot find convert or sips executables"; exit 1; echo >&2 "Cannot find convert or sips executables"; exit 1;

View File

@ -8,8 +8,27 @@
set -e set -e
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; } HAVE_IMAGEMAGICK=
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; } HAVE_GRAPHICSMAGICK=
type convert &>/dev/null && type identify &>/dev/null && HAVE_IMAGEMAGICK=true
type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && HAVE_GRAPHICSMAGICK=true
if [[ -z "$HAVE_IMAGEMAGICK" && -z "$HAVE_GRAPHICSMAGICK" ]]; then
type convert >/dev/null 2>&1 || echo >&2 "Cannot find required ImageMagick 'convert' executable"
type identify >/dev/null 2>&1 || echo >&2 "Cannot find required ImageMagick 'identify' executable"
type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && echo >&2 "Cannot find GraphicsMagick"
echo >&2 "ImageMagic or GraphicsMagic is required, please ensure they are in your PATH"
exit 1
fi
CONVERT="convert"
IDENTIFY="identify"
if [[ -z "$HAVE_IMAGEMAGICK" ]]; then
# we must have GraphicsMagick then
CONVERT="gm convert"
IDENTIFY="gm identify"
fi
# Parameters # Parameters
SOURCE="$1" SOURCE="$1"
@ -41,9 +60,9 @@ mkdir -p "${TEMP_DIR}"
# check if .ico is a sequence # check if .ico is a sequence
# pipe into cat so no exit code is given for grep if no matches are found # pipe into cat so no exit code is given for grep if no matches are found
IS_ICO_SET="$(identify "${SOURCE}" | grep -e "\w\.ico\[0" | cat )" IS_ICO_SET="$($IDENTIFY "${SOURCE}" | grep -e "\w\.ico\[0" | cat )"
convert "${SOURCE}" "${TEMP_DIR}/${BASE}.png" $CONVERT "${SOURCE}" "${TEMP_DIR}/${BASE}.png"
if [ "${IS_ICO_SET}" ]; then if [ "${IS_ICO_SET}" ]; then
# extract the largest(?) image from the set # extract the largest(?) image from the set
cp "${TEMP_DIR}/${BASE}-0.png" "${DEST}" cp "${TEMP_DIR}/${BASE}-0.png" "${DEST}"

32
icon-scripts/convertToTrayIcon Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# USAGE
# ./convertToTrayIcon <input png or icns> <outfilename>.png
# Example
# ./convertToTrayIcon ~/sample.icns ~/converted.png
set -e
SOURCE=$1
DEST=$2
if [ -z "${SOURCE}" ]; then
echo "No source image specified"
exit 1
fi
if [ -z "${DEST}" ]; then
echo "No destination specified"
exit 1
fi
NAME=$(basename "${SOURCE}")
EXT="${NAME##*.}"
if [ "${EXT}" == "png" ]; then
cp "${SOURCE}" "${DEST}"
exit 0
fi
sips --setProperty format png --resampleHeightWidth "256" "256" "${SOURCE}" --out "${DEST}"

8144
npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,14 @@
{ {
"name": "nativefier", "name": "nativefier",
"version": "42.0.1", "version": "52.0.0",
"description": "Wrap web apps natively", "description": "Wrap web apps natively",
"license": "MIT", "license": "MIT",
"author": "Goh Jia Hao", "author": "Goh Jia Hao",
"engines_README": "Bumping the minimum required Node version? You must bump: 1. package.json -> engines.node, 2. package.json -> devDependencies.@types/node , 3. tsconfig.json -> {target, lib} , 4. .github/workflows/ci.yml -> node-version",
"engines_READMEforEnginesNode": "Here in engines.node, we require a version as old as possible, for Nativefier to be easily installable using the stock Node.js shipped by conservative Linux distros. It's a balancing act between this, and our own dependencies requiring more a recent Node; as much as possible, try to keep supporting Debian stable; https://packages.debian.org/search?suite=stable&keywords=nodejs",
"engines": { "engines": {
"node": ">= 10.0.0", "node": ">= 16.16.0",
"npm": ">= 6.0.0" "npm": ">= 8.11.0"
}, },
"keywords": [ "keywords": [
"desktop", "desktop",
@ -16,89 +18,128 @@
"wrapper" "wrapper"
], ],
"main": "lib/main.js", "main": "lib/main.js",
"typings": "lib/main.d.ts",
"bin": { "bin": {
"nativefier": "lib/cli.js" "nativefier": "lib/cli.js"
}, },
"homepage": "https://github.com/jiahaog/nativefier", "homepage": "https://github.com/nativefier/nativefier",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/jiahaog/nativefier.git" "url": "git+https://github.com/nativefier/nativefier.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/jiahaog/nativefier/issues" "url": "https://github.com/nativefier/nativefier/issues"
}, },
"scripts": { "scripts": {
"build-app": "cd app && webpack", "build-app": "cd app && webpack",
"build-app-static": "ncp app/src/static/ app/lib/static/ && ncp app/dist/preload.js app/lib/preload.js && ncp app/dist/preload.js.map app/lib/preload.js.map", "build-app-static": "ncp app/src/static/ app/lib/static/ && ncp app/dist/preload.js app/lib/preload.js && ncp app/dist/preload.js.map app/lib/preload.js.map",
"build": "npm run clean && tsc --build . app && npm run build-app && npm run build-app-static", "build": "npm run clean && tsc --build shared src app && npm run build-app && npm run build-app-static",
"build:watch": "tsc --build . app --watch", "build:watch": "npm run clean && tsc --build shared src app --watch",
"dev-up": "npm install && cd ./app && npm install && cd ..", "changelog": "./.github/generate-changelog",
"dev-up-win": "npm install & cd app & npm install & cd ..", "clean": "rimraf coverage/ lib/ app/lib/ app/dist/ shared/lib",
"changelog": "./docs/generate-changelog", "clean:full": "npm run clean && rimraf app/node_modules/ node_modules/",
"ci": "npm run lint && npm test", "lint:fix": "cd src && eslint . --ext .ts --fix && cd ../shared && eslint src --ext .ts --fix && cd ../app && eslint src --ext .ts --fix",
"clean": "rimraf lib/ app/lib/ app/dist/", "lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts' 'shared/src/**/*.ts'",
"clean:full": "rimraf lib/ app/lib/ app/dist/ node_modules/ app/node_modules/", "lint": "eslint shared app src --ext .ts",
"lint:fix": "eslint . --fix", "list-outdated-deps": "npm out -l; cd app && npm out -l; true",
"lint:format": "prettier --write 'src/**/*.js' 'app/src/**/*.js'", "prepare": "cd app && npm ci && cd .. && npm run build",
"lint": "eslint . --ext .ts", "relock:cli": "rm -rf ./node_modules/ ./npm-shrinkwrap.json && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out -l",
"list-outdated-deps": "npm out; cd app && npm out; true", "relock:app": "rm -rf ./app/node_modules/ ./app/npm-shrinkwrap.json && cd app && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out -l",
"prepare": "cd ./app && npm install && cd .. && npm run build", "relock": "npm run relock:cli; npm run relock:app",
"test:integration": "jest --testRegex '.*integration-test.js'", "test:integration": "jest --testRegex=integration-test",
"test:manual": "npm run build && ./docs/manual-test", "test:manual": "npm run build && bash .github/manual-test",
"test:playwright": "jest --detectOpenHandles --testRegex=playwright-test",
"test:noplaywright": "jest --testPathIgnorePatterns=playwright",
"test:unit": "jest", "test:unit": "jest",
"test:watch": "jest --watch", "test:watch": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false",
"test:watch:unit": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false --testPathIgnorePatterns=integration --testPathIgnorePatterns=playwright",
"test:withlog": "LOGLEVEL=trace npm run test", "test:withlog": "LOGLEVEL=trace npm run test",
"test": "jest --testRegex '[-.]test\\.js$'", "test": "jest",
"watch": "npx concurrently \"npm:*:watch\"" "watch": "npx concurrently \"npm:*:watch\""
}, },
"dependencies": { "dependencies": {
"@types/cheerio": "0.x", "@electron/asar": "^3.2.4",
"@types/lodash": "4.x", "axios": "^1.4.0",
"@types/ncp": "2.x", "electron-packager": "^17.1.1",
"@types/node": "10.x", "fs-extra": "^11.1.1",
"@types/page-icon": "0.x", "gitcloud": "^0.2.4",
"@types/shelljs": "0.x", "hasbin": "^1.2.3",
"@types/tmp": "0.x", "loglevel": "^1.8.1",
"axios": "0.x", "ncp": "^2.0.0",
"cheerio": "^1.0.0-rc.3", "page-icon": "^0.4.0",
"commander": "4.x", "sanitize-filename": "^1.6.3",
"electron-packager": "15.x", "source-map-support": "^0.5.21",
"gitcloud": "0.x", "tmp": "^0.2.1",
"hasbin": "1.x", "yargs": "^17.7.2"
"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"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "26.x", "@types/debug": "^4.1.8",
"@typescript-eslint/eslint-plugin": "4.x", "@types/fs-extra": "^11.0.1",
"@typescript-eslint/parser": "4.x", "@types/hasbin": "^1.2.0",
"eslint": "7.x", "@types/jest": "^29.5.4",
"eslint-config-prettier": "7.x", "@types/ncp": "^2.0.5",
"eslint-plugin-prettier": "3.x", "@types/node": "^20.5.6",
"jest": "26.x", "@types/page-icon": "^0.3.4",
"prettier": "2.x", "@types/tmp": "^0.2.3",
"rimraf": "3.x", "@types/yargs": "^17.0.24",
"ts-loader": "8.x", "@typescript-eslint/eslint-plugin": "^6.4.1",
"typescript": "4.x", "@typescript-eslint/parser": "^6.4.1",
"webpack": "5.x", "electron": "^25.7.0",
"webpack-cli": "4.x" "eslint": "^8.46.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.6.2",
"playwright": "^1.36.2",
"prettier": "^3.0.1",
"rimraf": "^5.0.1",
"ts-loader": "^9.4.4",
"typescript": "^5.1.6",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
},
"jest_COMMENTS": {
"testPathIgnorePatterns": "See https://jestjs.io/docs/configuration#testpathignorepatterns-arraystring . We set it to 1. ignore coverage for deps, and 2. be sure we test the compiled JS, which is in `lib`, not `src` or `dist`",
"watchPathIgnorePatterns": "See https://jestjs.io/docs/configuration#watchpathignorepatterns-arraystring . We set it for `jest --watch` (a.k.a. `npm run test:watch`) to trigger only after `tsc --watch` (a.k.a. `npm run build:watch`) completes its incremental compilation. Else, jest will pick up immediately on changes in `src` when TSC is barely running, hence testing not-recompiled-yet code and being super confusing, as 1. your changes won't be taken during this first run, and 2. the *next* run (e.g. after a second 'Save' in your editor) will actually have the new code :D"
}, },
"jest": { "jest": {
"collectCoverage": true, "collectCoverage": true,
"collectCoverageFrom": [
"./app/dist/**/*.js",
"./lib/**/*.js",
"./shared/lib/**/*.js"
],
"coveragePathIgnorePatterns": [
"[.-]test.js$"
],
"moduleNameMapper": {
"^electron$": "<rootDir>/app/dist/mocks/electron.js"
},
"setupFiles": [ "setupFiles": [
"./lib/jestSetupFiles" "./lib/jestSetupFiles"
], ],
"testEnvironment": "node", "testEnvironment": "node",
"testPathIgnorePatterns": [ "testPathIgnorePatterns": [
"/node_modules/", "<rootDir>/app/node_modules.*",
"<rootDir>/app/src.*", "<rootDir>/app/src.*",
"<rootDir>/src.*" "<rootDir>/app/lib.*",
"<rootDir>/src.*",
".+\\.d\\.ts",
".+\\.js\\.map"
],
"testRegex": "test\\.js",
"testTimeout": 15000,
"watchPathIgnorePatterns": [
"<rootDir>/app/lib.*",
"<rootDir>/app/src.*",
"<rootDir>/app/tsconfig.json",
"<rootDir>/shared/tsconfig.json",
"<rootDir>/src.*",
"<rootDir>/tsconfig-base.json"
] ]
},
"prettier": {
"arrowParens": "always",
"singleQuote": true,
"trailingComma": "all"
} }
} }

14
shared/.eslintrc.js Normal file
View File

@ -0,0 +1,14 @@
const base = require('../base-eslintrc');
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
module.exports = {
parser: base.parser,
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
plugins: base.plugins,
extends: base.extends,
rules: base.rules,
ignorePatterns: ['lib/**'],
};

238
shared/src/options/model.ts Normal file
View File

@ -0,0 +1,238 @@
import { CreateOptions } from '@electron/asar';
import { randomUUID } from 'crypto';
import * as electronPackager from 'electron-packager';
export type TitleBarValue =
| 'default'
| 'hidden'
| 'hiddenInset'
| 'customButtonsOnHover';
export type TrayValue = 'true' | 'false' | 'start-in-tray';
export interface ElectronPackagerOptions extends electronPackager.Options {
arch: string;
portable: boolean;
platform?: string;
targetUrl: string;
upgrade: boolean;
upgradeFrom?: string;
}
export interface AppOptions {
packager: ElectronPackagerOptions;
nativefier: {
accessibilityPrompt: boolean;
alwaysOnTop: boolean;
backgroundColor?: string;
basicAuthPassword?: string;
basicAuthUsername?: string;
blockExternalUrls: boolean;
bookmarksMenu?: string;
bounce: boolean;
browserwindowOptions?: BrowserWindowOptions;
clearCache: boolean;
counter: boolean;
crashReporter?: string;
disableContextMenu: boolean;
disableDevTools: boolean;
disableGpu: boolean;
disableOldBuildWarning: boolean;
diskCacheSize?: number;
electronVersionUsed?: string;
enableEs3Apis: boolean;
fastQuit: boolean;
fileDownloadOptions?: Record<string, unknown>;
flashPluginDir?: string;
fullScreen: boolean;
globalShortcuts?: GlobalShortcut[];
hideWindowFrame: boolean;
ignoreCertificate: boolean;
ignoreGpuBlacklist: boolean;
inject?: string[];
insecure: boolean;
internalUrls?: string;
lang?: string;
maximize: boolean;
nativefierVersion: string;
processEnvs?: string;
proxyRules?: string;
quiet?: boolean;
showMenuBar: boolean;
singleInstance: boolean;
strictInternalUrls: boolean;
titleBarStyle?: TitleBarValue;
tray: TrayValue;
userAgent?: string;
userAgentHonest: boolean;
verbose: boolean;
versionString?: string;
width?: number;
widevine: boolean;
height?: number;
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
x?: number;
y?: number;
zoom: number;
};
}
export type BrowserWindowOptions = Record<string, unknown> & {
webPreferences?: Record<string, unknown>;
};
export type GlobalShortcut = {
key: string;
inputEvents: {
type:
| 'mouseDown'
| 'mouseUp'
| 'mouseEnter'
| 'mouseLeave'
| 'contextMenu'
| 'mouseWheel'
| 'mouseMove'
| 'keyDown'
| 'keyUp'
| 'char';
keyCode: string;
}[];
};
export type NativefierOptions = Partial<
AppOptions['packager'] & AppOptions['nativefier']
>;
export type OutputOptions = NativefierOptions & {
blockExternalUrls: boolean;
browserwindowOptions?: BrowserWindowOptions;
buildDate: number;
companyName?: string;
disableDevTools: boolean;
fileDownloadOptions?: Record<string, unknown>;
internalUrls: string | RegExp | undefined;
isUpgrade: boolean;
name: string;
nativefierVersion: string;
oldBuildWarningText: string;
strictInternalUrls: boolean;
tabbingIdentifier?: string;
targetUrl: string;
userAgent?: string;
zoom?: number;
};
export type PackageJSON = {
name: string;
};
export type RawOptions = {
accessibilityPrompt?: boolean;
alwaysOnTop?: boolean;
appCopyright?: string;
appVersion?: string;
arch?: string;
asar?: boolean | CreateOptions;
backgroundColor?: string;
basicAuthPassword?: string;
basicAuthUsername?: string;
blockExternalUrls?: boolean;
bookmarksMenu?: string;
bounce?: boolean;
browserwindowOptions?: BrowserWindowOptions;
buildVersion?: string;
clearCache?: boolean;
conceal?: boolean;
counter?: boolean;
crashReporter?: string;
darwinDarkModeSupport?: boolean;
disableContextMenu?: boolean;
disableDevTools?: boolean;
disableGpu?: boolean;
disableOldBuildWarning?: boolean;
disableOldBuildWarningYesiknowitisinsecure?: boolean;
diskCacheSize?: number;
electronVersion?: string;
electronVersionUsed?: string;
enableEs3Apis?: boolean;
fastQuit?: boolean;
fileDownloadOptions?: Record<string, unknown>;
flashPath?: string;
flashPluginDir?: string;
fullScreen?: boolean;
globalShortcuts?: string | GlobalShortcut[];
height?: number;
hideWindowFrame?: boolean;
icon?: string;
ignoreCertificate?: boolean;
ignoreGpuBlacklist?: boolean;
inject?: string[];
insecure?: boolean;
internalUrls?: string;
lang?: string;
maxHeight?: number;
maximize?: boolean;
maxWidth?: number;
minHeight?: number;
minWidth?: number;
name?: string;
nativefierVersion?: string;
out?: string;
overwrite?: boolean;
platform?: string;
portable?: boolean;
processEnvs?: string;
proxyRules?: string;
quiet?: boolean;
showMenuBar?: boolean;
singleInstance?: boolean;
strictInternalUrls?: boolean;
targetUrl?: string;
titleBarStyle?: TitleBarValue;
tray?: TrayValue;
upgrade?: string | boolean;
upgradeFrom?: string;
userAgent?: string;
userAgentHonest?: boolean;
verbose?: boolean;
versionString?: string;
widevine?: boolean;
width?: number;
win32metadata?: electronPackager.Win32MetadataOptions;
x?: number;
y?: number;
zoom?: number;
};
export type WindowOptions = {
autoHideMenuBar: boolean;
blockExternalUrls: boolean;
browserwindowOptions?: BrowserWindowOptions;
insecure: boolean;
internalUrls?: string | RegExp;
strictInternalUrls?: boolean;
name: string;
proxyRules?: string;
show?: boolean;
tabbingIdentifier?: string;
targetUrl: string;
userAgent?: string;
zoom: number;
};
export function outputOptionsToWindowOptions(
options: OutputOptions,
generateTabbingIdentifierIfMissing: boolean,
): WindowOptions {
return {
...options,
autoHideMenuBar: !options.showMenuBar,
insecure: options.insecure ?? false,
tabbingIdentifier: generateTabbingIdentifierIfMissing
? options.tabbingIdentifier ?? randomUUID()
: options.tabbingIdentifier,
zoom: options.zoom ?? 1.0,
};
}

18
shared/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"composite": true,
"outDir": "./lib",
// Here we want to set target and lib to the *worst* of app/tsconfig.json and src/tsconfig.json
// (plus "dom"), because shared code will run both in CLI Node and app Node.
// See comments in app/tsconfig.json and src/tsconfig.json
"target": "es2018",
"lib": [
"es2018",
"dom"
]
},
"include": [
"./src/**/*"
],
}

13
src/.eslintrc.js Normal file
View File

@ -0,0 +1,13 @@
const base = require('../base-eslintrc');
// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
module.exports = {
parser: base.parser,
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
plugins: base.plugins,
extends: base.extends,
rules: base.rules,
};

View File

@ -7,8 +7,9 @@ import {
convertToPng, convertToPng,
convertToIco, convertToIco,
convertToIcns, convertToIcns,
convertToTrayIcon,
} from '../helpers/iconShellHelpers'; } from '../helpers/iconShellHelpers';
import { AppOptions } from '../options/model'; import { AppOptions } from '../../shared/src/options/model';
function iconIsIco(iconPath: string): boolean { function iconIsIco(iconPath: string): boolean {
return path.extname(iconPath) === '.ico'; return path.extname(iconPath) === '.ico';
@ -26,9 +27,7 @@ function iconIsIcns(iconPath: string): boolean {
* Will convert a `.png` icon to the appropriate arch format (if necessary), * Will convert a `.png` icon to the appropriate arch format (if necessary),
* and return adjusted options * and return adjusted options
*/ */
export async function convertIconIfNecessary( export function convertIconIfNecessary(options: AppOptions): void {
options: AppOptions,
): Promise<void> {
if (!options.packager.icon) { if (!options.packager.icon) {
log.debug('Option "icon" not set, skipping icon conversion.'); log.debug('Option "icon" not set, skipping icon conversion.');
return; return;
@ -43,11 +42,11 @@ export async function convertIconIfNecessary(
} }
try { try {
const iconPath = await convertToIco(options.packager.icon); const iconPath = convertToIco(options.packager.icon);
options.packager.icon = iconPath; options.packager.icon = iconPath;
return; return;
} catch (error) { } catch (err: unknown) {
log.warn('Failed to convert icon to .ico, skipping.', error); log.warn('Failed to convert icon to .ico, skipping.', err);
return; return;
} }
} }
@ -61,11 +60,11 @@ export async function convertIconIfNecessary(
} }
try { try {
const iconPath = await convertToPng(options.packager.icon); const iconPath = convertToPng(options.packager.icon);
options.packager.icon = iconPath; options.packager.icon = iconPath;
return; return;
} catch (error) { } catch (err: unknown) {
log.warn('Failed to convert icon to .png, skipping.', error); log.warn('Failed to convert icon to .png, skipping.', err);
return; return;
} }
} }
@ -74,7 +73,6 @@ export async function convertIconIfNecessary(
log.debug( log.debug(
'Building for macOS and icon is already a .icns, no conversion needed', 'Building for macOS and icon is already a .icns, no conversion needed',
); );
return;
} }
if (!isOSX()) { if (!isOSX()) {
@ -85,12 +83,15 @@ export async function convertIconIfNecessary(
} }
try { try {
const iconPath = await convertToIcns(options.packager.icon); if (!iconIsIcns(options.packager.icon)) {
const iconPath = convertToIcns(options.packager.icon);
options.packager.icon = iconPath; options.packager.icon = iconPath;
return; }
} catch (error) { if (options.nativefier.tray !== 'false') {
log.warn('Failed to convert icon to .icns, skipping.', error); convertToTrayIcon(options.packager.icon);
}
} catch (err: unknown) {
log.warn('Failed to convert icon to .icns, skipping.', err);
options.packager.icon = undefined; options.packager.icon = undefined;
return;
} }
} }

View File

@ -1,15 +1,21 @@
import * as path from 'path'; import * as path from 'path';
import * as electronGet from '@electron/get'; import * as electronGet from '@electron/get';
import * as electronPackager from 'electron-packager'; import electronPackager from 'electron-packager';
import * as hasbin from 'hasbin'; import * as fs from 'fs-extra';
import * as log from 'loglevel'; import * as log from 'loglevel';
import { isWindows, getTempDir, copyFileOrDir } from '../helpers/helpers'; import { convertIconIfNecessary } from './buildIcon';
import {
getTempDir,
hasWine,
isWindows,
isWindowsAdmin,
} from '../helpers/helpers';
import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade';
import { AppOptions, RawOptions } from '../../shared/src/options/model';
import { getOptions } from '../options/optionsMain'; import { getOptions } from '../options/optionsMain';
import { prepareElectronApp } from './prepareElectronApp'; import { prepareElectronApp } from './prepareElectronApp';
import { convertIconIfNecessary } from './buildIcon';
import { AppOptions, NativefierOptions } from '../options/model';
const OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD = [ const OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD = [
'icon', 'icon',
@ -20,28 +26,6 @@ const OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD = [
'win32metadata', '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 * 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 * folder, which the BrowserWindow is hard-coded to read the icon from
@ -60,7 +44,18 @@ async function copyIconsIfNecessary(
options.packager.platform === 'darwin' || options.packager.platform === 'darwin' ||
options.packager.platform === 'mas' options.packager.platform === 'mas'
) { ) {
if (options.nativefier.tray !== 'false') {
//tray icon needs to be .png
log.debug('Copying icon for tray application');
const trayIconFileName = `tray-icon.png`;
const destIconPath = path.join(appPath, 'icon.png');
await fs.copy(
`${path.dirname(options.packager.icon)}/${trayIconFileName}`,
destIconPath,
);
} else {
log.debug('No copying necessary on macOS; aborting'); log.debug('No copying necessary on macOS; aborting');
}
return; return;
} }
@ -69,15 +64,46 @@ async function copyIconsIfNecessary(
const destIconPath = path.join(appPath, destFileName); const destIconPath = path.join(appPath, destFileName);
log.debug(`Copying icon ${options.packager.icon} to`, destIconPath); log.debug(`Copying icon ${options.packager.icon} to`, destIconPath);
await copyFileOrDir(options.packager.icon, destIconPath); await fs.copy(options.packager.icon, destIconPath);
}
/**
* Checks the app path array to determine if packaging completed successfully
*/
function getAppPath(appPath: string | string[]): string | undefined {
if (!Array.isArray(appPath)) {
return appPath;
}
if (appPath.length === 0) {
return undefined; // 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];
}
function isUpgrade(rawOptions: RawOptions): boolean {
if (
rawOptions.upgrade !== undefined &&
typeof rawOptions.upgrade === 'string' &&
rawOptions.upgrade !== ''
) {
rawOptions.upgradeFrom = rawOptions.upgrade;
rawOptions.upgrade = true;
return true;
}
return false;
} }
function trimUnprocessableOptions(options: AppOptions): void { function trimUnprocessableOptions(options: AppOptions): void {
if ( if (options.packager.platform === 'win32' && !isWindows() && !hasWine()) {
options.packager.platform === 'win32' &&
!isWindows() &&
!hasbin.sync('wine')
) {
const optionsPresent = Object.entries(options) const optionsPresent = Object.entries(options)
.filter( .filter(
([key, value]) => ([key, value]) =>
@ -94,27 +120,89 @@ function trimUnprocessableOptions(options: AppOptions): void {
'features, like a correct icon and process name. Do yourself a favor and install Wine, please.', 'features, like a correct icon and process name. Do yourself a favor and install Wine, please.',
); );
for (const keyToUnset of optionsPresent) { for (const keyToUnset of optionsPresent) {
options[keyToUnset] = null; (options as unknown as Record<string, undefined>)[keyToUnset] = undefined;
} }
} }
} }
function getOSRunHelp(platform?: string): string {
if (platform === 'win32') {
return `the contained .exe file.`;
} else if (platform === 'linux') {
return `the contained executable file (prefixing with ./ if necessary)\nMenu/desktop shortcuts are up to you, because Nativefier cannot know where you're going to move the app. Search for "linux .desktop file" for help, or see https://wiki.archlinux.org/index.php/Desktop_entries`;
} else if (platform === 'darwin') {
return `the app bundle.`;
}
return '';
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function buildNativefierApp( export async function buildNativefierApp(
rawOptions: NativefierOptions, rawOptions: RawOptions,
): Promise<string> { ): Promise<string> {
log.info('Processing options...'); // early-suppress potential logging before full options handling
if (rawOptions.quiet) {
log.setLevel('silent');
}
log.warn(
'\n\n Hi! Nativefier is minimally maintained these days, and needs more hands.\n' +
' If you have the time & motivation, help with bugfixes and maintenance is VERY welcome.\n' +
' Please go to https://github.com/nativefier/nativefier and help how you can. Thanks.\n\n',
);
log.info('\nProcessing options...');
let finalOutDirectory = rawOptions.out ?? process.cwd();
if (isUpgrade(rawOptions)) {
log.debug('Attempting to upgrade from', rawOptions.upgradeFrom);
const oldApp = findUpgradeApp(rawOptions.upgradeFrom as string);
if (!oldApp) {
throw new Error(
`Could not find an old Nativfier app in "${
rawOptions.upgradeFrom as string
}"`,
);
}
rawOptions = useOldAppOptions(rawOptions, oldApp);
if (rawOptions.out === undefined && rawOptions.overwrite) {
finalOutDirectory = oldApp.appRoot;
rawOptions.out = getTempDir('appUpgrade', 0o755);
}
}
log.debug('rawOptions', rawOptions);
const options = await getOptions(rawOptions); const options = await getOptions(rawOptions);
log.debug('options', options);
if (options.packager.platform === 'darwin' && isWindows()) {
// electron-packager has to extract the desired electron package for the target platform.
// For a target platform of Mac, this zip file contains symlinks. And on Windows, extracting
// files that are symlinks need Admin permissions. So we'll check if the user is an admin, and
// fail early if not.
// For reference
// https://github.com/electron/electron-packager/issues/933
// https://github.com/electron/electron-packager/issues/1194
// https://github.com/electron/electron/issues/11094
if (!isWindowsAdmin()) {
throw new Error(
'Building an app with a target platform of Mac on a Windows machine requires admin priveleges to perform. Please rerun this command in an admin command prompt.',
);
}
}
log.info('\nPreparing Electron app...'); log.info('\nPreparing Electron app...');
const tmpPath = getTempDir('app', 0o755); const tmpPath = getTempDir('app', 0o755);
await prepareElectronApp(options.packager.dir, tmpPath, options); await prepareElectronApp(options.packager.dir, tmpPath, options);
log.info('\nConverting icons...'); log.info('\nConverting icons...');
options.packager.dir = tmpPath; // const optionsWithTmpPath = { ...options, dir: tmpPath }; options.packager.dir = tmpPath;
await convertIconIfNecessary(options); convertIconIfNecessary(options);
await copyIconsIfNecessary(options, tmpPath); await copyIconsIfNecessary(options, tmpPath);
options.packager.quiet = !rawOptions.verbose;
log.info( log.info(
"\nPackaging... This will take a few seconds, maybe minutes if the requested Electron isn't cached yet...", "\nPackaging... This will take a few seconds, maybe minutes if the requested Electron isn't cached yet...",
); );
@ -123,20 +211,57 @@ export async function buildNativefierApp(
const appPathArray = await electronPackager(options.packager); const appPathArray = await electronPackager(options.packager);
log.info('\nFinalizing build...'); log.info('\nFinalizing build...');
const appPath = getAppPath(appPathArray); let appPath = getAppPath(appPathArray);
if (appPath) { if (!appPath) {
let osRunHelp = ''; throw new Error('App Path could not be determined.');
if (options.packager.platform === 'win32') {
osRunHelp = `the contained .exe file.`;
} else if (options.packager.platform === 'linux') {
osRunHelp = `the contained executable file (prefixing with ./ if necessary)\nMenu/desktop shortcuts are up to you, because Nativefier cannot know where you're going to move the app. Search for "linux .desktop file" for help, or see https://wiki.archlinux.org/index.php/Desktop_entries`;
} else if (options.packager.platform === 'darwin') {
osRunHelp = `the app bundle.`;
} }
log.info(
`App built to ${appPath} , move it wherever it makes sense for you and run ${osRunHelp}`, if (
options.packager.upgrade &&
options.packager.upgradeFrom &&
options.packager.overwrite
) {
if (options.packager.platform === 'darwin') {
try {
// This is needed due to a funky thing that happens when copying Squirrel.framework
// over where it gets into a circular file reference somehow.
await fs.remove(
path.join(
finalOutDirectory,
`${options.packager.name ?? ''}.app`,
'Contents',
'Frameworks',
),
);
} catch (err: unknown) {
log.warn(
'Encountered an error when attempting to pre-delete old frameworks:',
err,
); );
} }
await fs.copy(
path.join(appPath, `${options.packager.name ?? ''}.app`),
path.join(finalOutDirectory, `${options.packager.name ?? ''}.app`),
{
overwrite: options.packager.overwrite,
preserveTimestamps: true,
},
);
} else {
await fs.copy(appPath, finalOutDirectory, {
overwrite: options.packager.overwrite,
preserveTimestamps: true,
});
}
await fs.remove(appPath);
appPath = finalOutDirectory;
}
const osRunHelp = getOSRunHelp(options.packager.platform);
log.info(
`App built to ${appPath}, move to wherever it makes sense for you and run ${osRunHelp}`,
);
return appPath; return appPath;
} }

View File

@ -0,0 +1,11 @@
import { normalizeAppName } from './prepareElectronApp';
describe('normalizeAppName', () => {
test('it is stable', () => {
// Non-determinism / unstability would cause using a different appName
// at each app regen, thus a different appData folder, which would cause
// losing user state, including login state through cookies.
const normalizedTrello = normalizeAppName('Trello', 'https://trello.com');
expect(normalizedTrello).toBe('trello-nativefier-679e8e');
});
});

View File

@ -1,83 +1,119 @@
import * as fs from 'fs';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as fs from 'fs-extra';
import * as path from 'path'; import * as path from 'path';
import { promisify } from 'util';
import { kebabCase } from 'lodash';
import * as log from 'loglevel'; import * as log from 'loglevel';
import { copyFileOrDir } from '../helpers/helpers'; import { generateRandomSuffix } from '../helpers/helpers';
import { AppOptions } from '../options/model'; import {
AppOptions,
const writeFileAsync = promisify(fs.writeFile); OutputOptions,
PackageJSON,
} from '../../shared/src/options/model';
import { parseJson } from '../utils/parseUtils';
import { DEFAULT_APP_NAME } from '../constants';
/** /**
* Only picks certain app args to pass to nativefier.json * Only picks certain app args to pass to nativefier.json
*/ */
function pickElectronAppArgs(options: AppOptions): any { function pickElectronAppArgs(options: AppOptions): OutputOptions {
return { return {
accessibilityPrompt: options.nativefier.accessibilityPrompt,
alwaysOnTop: options.nativefier.alwaysOnTop, alwaysOnTop: options.nativefier.alwaysOnTop,
appBundleId: options.packager.appBundleId,
appCategoryType: options.packager.appCategoryType,
appCopyright: options.packager.appCopyright, appCopyright: options.packager.appCopyright,
appVersion: options.packager.appVersion, appVersion: options.packager.appVersion,
arch: options.packager.arch,
asar: options.packager.asar,
backgroundColor: options.nativefier.backgroundColor, backgroundColor: options.nativefier.backgroundColor,
basicAuthPassword: options.nativefier.basicAuthPassword, basicAuthPassword: options.nativefier.basicAuthPassword,
basicAuthUsername: options.nativefier.basicAuthUsername, basicAuthUsername: options.nativefier.basicAuthUsername,
blockExternalUrls: options.nativefier.blockExternalUrls,
bounce: options.nativefier.bounce, bounce: options.nativefier.bounce,
browserwindowOptions: options.nativefier.browserwindowOptions, browserwindowOptions: options.nativefier.browserwindowOptions,
buildDate: new Date().getTime(),
buildVersion: options.packager.buildVersion, buildVersion: options.packager.buildVersion,
clearCache: options.nativefier.clearCache, clearCache: options.nativefier.clearCache,
counter: options.nativefier.counter, counter: options.nativefier.counter,
crashReporter: options.nativefier.crashReporter, crashReporter: options.nativefier.crashReporter,
darwinDarkModeSupport: options.packager.darwinDarkModeSupport, darwinDarkModeSupport: options.packager.darwinDarkModeSupport,
derefSymlinks: options.packager.derefSymlinks,
disableContextMenu: options.nativefier.disableContextMenu, disableContextMenu: options.nativefier.disableContextMenu,
disableDevTools: options.nativefier.disableDevTools, disableDevTools: options.nativefier.disableDevTools,
disableGpu: options.nativefier.disableGpu, disableGpu: options.nativefier.disableGpu,
disableOldBuildWarning: options.nativefier.disableOldBuildWarning,
diskCacheSize: options.nativefier.diskCacheSize, diskCacheSize: options.nativefier.diskCacheSize,
download: options.packager.download,
electronVersionUsed: options.packager.electronVersion,
enableEs3Apis: options.nativefier.enableEs3Apis, enableEs3Apis: options.nativefier.enableEs3Apis,
executableName: options.packager.executableName,
fastQuit: options.nativefier.fastQuit, fastQuit: options.nativefier.fastQuit,
fileDownloadOptions: options.nativefier.fileDownloadOptions, fileDownloadOptions: options.nativefier.fileDownloadOptions,
flashPluginDir: options.nativefier.flashPluginDir, flashPluginDir: options.nativefier.flashPluginDir,
fullScreen: options.nativefier.fullScreen, fullScreen: options.nativefier.fullScreen,
globalShortcuts: options.nativefier.globalShortcuts, globalShortcuts: options.nativefier.globalShortcuts,
height: options.nativefier.height, height: options.nativefier.height,
helperBundleId: options.packager.helperBundleId,
hideWindowFrame: options.nativefier.hideWindowFrame, hideWindowFrame: options.nativefier.hideWindowFrame,
ignoreCertificate: options.nativefier.ignoreCertificate, ignoreCertificate: options.nativefier.ignoreCertificate,
ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist, ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist,
insecure: options.nativefier.insecure, insecure: options.nativefier.insecure,
internalUrls: options.nativefier.internalUrls, internalUrls: options.nativefier.internalUrls,
blockExternalUrls: options.nativefier.blockExternalUrls, isUpgrade: options.packager.upgrade,
maxHeight: options.nativefier.maxHeight, junk: options.packager.junk,
lang: options.nativefier.lang,
maximize: options.nativefier.maximize, maximize: options.nativefier.maximize,
maxHeight: options.nativefier.maxHeight,
maxWidth: options.nativefier.maxWidth, maxWidth: options.nativefier.maxWidth,
minHeight: options.nativefier.minHeight, minHeight: options.nativefier.minHeight,
minWidth: options.nativefier.minWidth, minWidth: options.nativefier.minWidth,
name: options.packager.name, name: options.packager.name ?? DEFAULT_APP_NAME,
nativefierVersion: options.nativefier.nativefierVersion, nativefierVersion: options.nativefier.nativefierVersion,
osxNotarize: options.packager.osxNotarize,
osxSign: options.packager.osxSign,
portable: options.packager.portable,
processEnvs: options.nativefier.processEnvs, processEnvs: options.nativefier.processEnvs,
protocols: options.packager.protocols,
proxyRules: options.nativefier.proxyRules, proxyRules: options.nativefier.proxyRules,
prune: options.packager.prune,
quiet: options.packager.quiet,
showMenuBar: options.nativefier.showMenuBar, showMenuBar: options.nativefier.showMenuBar,
singleInstance: options.nativefier.singleInstance, singleInstance: options.nativefier.singleInstance,
strictInternalUrls: options.nativefier.strictInternalUrls,
targetUrl: options.packager.targetUrl, targetUrl: options.packager.targetUrl,
titleBarStyle: options.nativefier.titleBarStyle, titleBarStyle: options.nativefier.titleBarStyle,
tray: options.nativefier.tray, tray: options.nativefier.tray,
usageDescription: options.packager.usageDescription,
userAgent: options.nativefier.userAgent, userAgent: options.nativefier.userAgent,
userAgentHonest: options.nativefier.userAgentHonest,
versionString: options.nativefier.versionString, versionString: options.nativefier.versionString,
width: options.nativefier.width, width: options.nativefier.width,
widevine: options.nativefier.widevine,
win32metadata: options.packager.win32metadata, win32metadata: options.packager.win32metadata,
disableOldBuildWarning: options.nativefier.disableOldBuildWarning,
x: options.nativefier.x, x: options.nativefier.x,
y: options.nativefier.y, y: options.nativefier.y,
zoom: options.nativefier.zoom, zoom: options.nativefier.zoom,
buildDate: new Date().getTime(), // OLD_BUILD_WARNING_TEXT is an undocumented env. var to let *packagers*
// tweak the message shown on warning about an old build, to something
// more tailored to their audience (who might not even know Nativefier).
// See https://github.com/kelyvin/Google-Messages-For-Desktop/issues/34#issuecomment-812731144
// and https://github.com/nativefier/nativefier/issues/1131#issuecomment-812646988
oldBuildWarningText: process.env.OLD_BUILD_WARNING_TEXT || '',
}; };
} }
async function maybeCopyScripts(srcs: string[], dest: string): Promise<void> { async function maybeCopyScripts(
srcs: string[] | undefined,
dest: string,
): Promise<void> {
if (!srcs || srcs.length === 0) { if (!srcs || srcs.length === 0) {
log.debug('No files to inject, skipping copy.'); log.debug('No files to inject, skipping copy.');
return; return;
} }
const supportedInjectionExtensions = ['.css', '.js'];
log.debug(`Copying ${srcs.length} files to inject in app.`); log.debug(`Copying ${srcs.length} files to inject in app.`);
for (const src of srcs) { for (const src of srcs) {
if (!fs.existsSync(src)) { if (!fs.existsSync(src)) {
@ -86,27 +122,33 @@ async function maybeCopyScripts(srcs: string[], dest: string): Promise<void> {
); );
} }
let destFileName: string; if (supportedInjectionExtensions.indexOf(path.extname(src)) < 0) {
if (path.extname(src) === '.js') { log.warn('Skipping unsupported injection file', src);
destFileName = 'inject.js'; continue;
} else if (path.extname(src) === '.css') {
destFileName = 'inject.css';
} else {
return;
} }
const postFixHash = generateRandomSuffix();
const destFileName = `inject-${postFixHash}${path.extname(src)}`;
const destPath = path.join(dest, 'inject', destFileName); const destPath = path.join(dest, 'inject', destFileName);
log.debug(`Copying injection file "${src}" to "${destPath}"`); log.debug(`Copying injection file "${src}" to "${destPath}"`);
await copyFileOrDir(src, destPath); await fs.copy(src, destPath);
} }
} }
function normalizeAppName(appName: string, url: string): string { /**
// use a simple 3 byte random string to prevent collision * Use a basic 6-character hash to prevent collisions. The hash is deterministic url & name,
* so that an upgrade (same URL) of an app keeps using the same appData folder.
* Warning! Changing this normalizing & hashing will change the way appNames are generated,
* changing appData folder, and users will get logged out of their apps after an upgrade.
*/
export function normalizeAppName(appName: string, url: string): string {
const hash = crypto.createHash('md5'); const hash = crypto.createHash('md5');
hash.update(url); hash.update(url);
const postFixHash = hash.digest('hex').substring(0, 6); const postFixHash = hash.digest('hex').substring(0, 6);
const normalized = kebabCase(appName.toLowerCase()); const normalized = appName
.toLowerCase()
.replace(/[,:.]/g, '')
.replace(/[\s_]/g, '-');
return `${normalized}-nativefier-${postFixHash}`; return `${normalized}-nativefier-${postFixHash}`;
} }
@ -114,14 +156,21 @@ function changeAppPackageJsonName(
appPath: string, appPath: string,
name: string, name: string,
url: string, url: string,
): void { ): string {
const packageJsonPath = path.join(appPath, '/package.json'); const packageJsonPath = path.join(appPath, '/package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); const packageJson = parseJson<PackageJSON>(
fs.readFileSync(packageJsonPath).toString(),
);
if (!packageJson) {
throw new Error(`Could not load package.json from ${packageJsonPath}`);
}
const normalizedAppName = normalizeAppName(name, url); const normalizedAppName = normalizeAppName(name, url);
packageJson.name = normalizedAppName; packageJson.name = normalizedAppName;
log.debug(`Updating ${packageJsonPath} 'name' field to ${normalizedAppName}`); log.debug(`Updating ${packageJsonPath} 'name' field to ${normalizedAppName}`);
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
return normalizedAppName;
} }
/** /**
@ -135,26 +184,36 @@ export async function prepareElectronApp(
): Promise<void> { ): Promise<void> {
log.debug(`Copying electron app from ${src} to ${dest}`); log.debug(`Copying electron app from ${src} to ${dest}`);
try { try {
await copyFileOrDir(src, dest); await fs.copy(src, dest);
} catch (err) { } catch (err: unknown) {
throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${(err as Error).toString()}`; throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${
(err as Error).message
}`;
} }
const appJsonPath = path.join(dest, '/nativefier.json'); const appJsonPath = path.join(dest, '/nativefier.json');
log.debug(`Writing app config to ${appJsonPath}`); const pickedOptions = pickElectronAppArgs(options);
await writeFileAsync( log.debug(`Writing app config to ${appJsonPath}`, pickedOptions);
appJsonPath, await fs.writeFile(appJsonPath, JSON.stringify(pickedOptions));
JSON.stringify(pickElectronAppArgs(options)),
); if (options.nativefier.bookmarksMenu) {
const bookmarksJsonPath = path.join(dest, '/bookmarks.json');
try {
await fs.copy(options.nativefier.bookmarksMenu, bookmarksJsonPath);
} catch (err: unknown) {
log.error('Error copying bookmarks menu config file.', err);
}
}
try { try {
await maybeCopyScripts(options.nativefier.inject, dest); await maybeCopyScripts(options.nativefier.inject, dest);
} catch (err) { } catch (err: unknown) {
log.error('Error copying injection files.', err); log.error('Error copying injection files.', err);
} }
changeAppPackageJsonName( const normalizedAppName = changeAppPackageJsonName(
dest, dest,
options.packager.name, options.packager.name as string,
options.packager.targetUrl, options.packager.targetUrl,
); );
options.packager.appBundleId = `com.electron.nativefier.${normalizedAppName}`;
} }

326
src/cli.test.ts Normal file
View File

@ -0,0 +1,326 @@
import 'source-map-support/register';
import { initArgs, parseArgs } from './cli';
import { parseJson } from './utils/parseUtils';
describe('initArgs + parseArgs', () => {
let mockExit: jest.SpyInstance;
beforeEach(() => {
mockExit = jest.spyOn(process, 'exit').mockImplementation();
});
afterEach(() => {
mockExit.mockRestore();
});
test('--help forces exit', () => {
// Mock console.log to not pollute the log with the yargs help text
const mockLog = jest.spyOn(console, 'log').mockImplementation();
initArgs(['https://www.google.com', '--help']);
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockLog).toBeCalled();
mockLog.mockRestore();
});
test('--version forces exit', () => {
// Mock console.log to not pollute the log with the yargs help text
const mockLog = jest.spyOn(console, 'log').mockImplementation();
initArgs(['https://www.google.com', '--version']);
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockLog).toBeCalled();
mockLog.mockRestore();
});
// Positional options
test('first positional becomes targetUrl', () => {
const args = parseArgs(initArgs(['https://google.com']));
expect(args.targetUrl).toBe('https://google.com');
expect(args.upgrade).toBeUndefined();
});
test('second positional becomes out', () => {
const args = parseArgs(initArgs(['https://google.com', 'tmp']));
expect(args.out).toBe('tmp');
expect(args.targetUrl).toBe('https://google.com');
expect(args.upgrade).toBeUndefined();
});
// App Creation Options
test('upgrade arg', () => {
const args = parseArgs(initArgs(['--upgrade', 'pathToUpgrade']));
expect(args.upgrade).toBe('pathToUpgrade');
expect(args.targetUrl).toBeUndefined();
});
test('upgrade arg with out dir', () => {
const args = parseArgs(initArgs(['tmp', '--upgrade', 'pathToUpgrade']));
expect(args.upgrade).toBe('pathToUpgrade');
expect(args.out).toBe('tmp');
expect(args.targetUrl).toBeUndefined();
});
test('upgrade arg with targetUrl', () => {
expect(() =>
parseArgs(
initArgs(['https://www.google.com', '--upgrade', 'path/to/upgrade']),
),
).toThrow();
});
test('multi-inject', () => {
const args = parseArgs(
initArgs([
'https://google.com',
'--inject',
'test.js',
'--inject',
'test2.js',
'--inject',
'test.css',
'--inject',
'test2.css',
]),
);
expect(args.inject).toEqual([
'test.js',
'test2.js',
'test.css',
'test2.css',
]);
});
test.each([
{ arg: 'app-copyright', shortArg: '', value: '(c) Nativefier' },
{ arg: 'app-version', shortArg: '', value: '2.0.0' },
{ arg: 'background-color', shortArg: '', value: '#FFAA88' },
{ arg: 'basic-auth-username', shortArg: '', value: 'user' },
{ arg: 'basic-auth-password', shortArg: '', value: 'p@ssw0rd' },
{ arg: 'bookmarks-menu', shortArg: '', value: 'bookmarks.json' },
{
arg: 'browserwindow-options',
shortArg: '',
value: '{"test": 456}',
isJsonString: true,
},
{ arg: 'build-version', shortArg: '', value: '3.0.0' },
{
arg: 'crash-reporter',
shortArg: '',
value: 'https://crash-reporter.com',
},
{ arg: 'electron-version', shortArg: 'e', value: '1.0.0' },
{
arg: 'file-download-options',
shortArg: '',
value: '{"test": 789}',
isJsonString: true,
},
{ arg: 'flash-path', shortArg: '', value: 'pathToFlash' },
{ arg: 'global-shortcuts', shortArg: '', value: 'shortcuts.json' },
{ arg: 'icon', shortArg: 'i', value: 'icon.png' },
{ arg: 'internal-urls', shortArg: '', value: '.*' },
{ arg: 'lang', shortArg: '', value: 'fr' },
{ arg: 'name', shortArg: 'n', value: 'Google' },
{
arg: 'process-envs',
shortArg: '',
value: '{"test": 123}',
isJsonString: true,
},
{ arg: 'proxy-rules', shortArg: '', value: 'RULE: PROXY' },
{ arg: 'tray', shortArg: '', value: 'true' },
{ arg: 'user-agent', shortArg: 'u', value: 'FIREFOX' },
{
arg: 'win32metadata',
shortArg: '',
value: '{"ProductName": "Google"}',
isJsonString: true,
},
])('test string arg %s', ({ arg, shortArg, value, isJsonString }) => {
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`, value]),
) as unknown as Record<string, string>;
if (!isJsonString) {
expect(args[arg]).toBe(value);
} else {
expect(args[arg]).toEqual(parseJson(value));
}
if (shortArg) {
const argsShort = parseArgs(
initArgs(['https://google.com', `-${shortArg}`, value]),
) as unknown as Record<string, string>;
if (!isJsonString) {
expect(argsShort[arg]).toBe(value);
} else {
expect(argsShort[arg]).toEqual(parseJson(value));
}
}
});
test.each([
{ arg: 'arch', shortArg: 'a', value: 'x64', badValue: '486' },
{ arg: 'platform', shortArg: 'p', value: 'mac', badValue: 'os2' },
{
arg: 'title-bar-style',
shortArg: '',
value: 'hidden',
badValue: 'cool',
},
])('limited choice arg %s', ({ arg, shortArg, value, badValue }) => {
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`, value]),
) as unknown as Record<string, string>;
expect(args[arg]).toBe(value);
// Mock console.error to not pollute the log with the yargs help text
const mockError = jest.spyOn(console, 'error').mockImplementation();
initArgs(['https://google.com', `--${arg}`, badValue]);
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockError).toBeCalled();
mockExit.mockClear();
mockError.mockClear();
if (shortArg) {
const argsShort = parseArgs(
initArgs(['https://google.com', `-${shortArg}`, value]),
) as unknown as Record<string, string>;
expect(argsShort[arg]).toBe(value);
initArgs(['https://google.com', `-${shortArg}`, badValue]);
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockError).toBeCalled();
}
mockError.mockRestore();
});
test.each([
{ arg: 'always-on-top', shortArg: '' },
{ arg: 'block-external-urls', shortArg: '' },
{ arg: 'bounce', shortArg: '' },
{ arg: 'clear-cache', shortArg: '' },
{ arg: 'conceal', shortArg: 'c' },
{ arg: 'counter', shortArg: '' },
{ arg: 'darwin-dark-mode-support', shortArg: '' },
{ arg: 'disable-context-menu', shortArg: '' },
{ arg: 'disable-dev-tools', shortArg: '' },
{ arg: 'disable-gpu', shortArg: '' },
{ arg: 'disable-old-build-warning-yesiknowitisinsecure', shortArg: '' },
{ arg: 'enable-es3-apis', shortArg: '' },
{ arg: 'fast-quit', shortArg: 'f' },
{ arg: 'flash', shortArg: '' },
{ arg: 'full-screen', shortArg: '' },
{ arg: 'hide-window-frame', shortArg: '' },
{ arg: 'honest', shortArg: '' },
{ arg: 'ignore-certificate', shortArg: '' },
{ arg: 'ignore-gpu-blacklist', shortArg: '' },
{ arg: 'insecure', shortArg: '' },
{ arg: 'maximize', shortArg: '' },
{ arg: 'portable', shortArg: '' },
{ arg: 'show-menu-bar', shortArg: 'm' },
{ arg: 'single-instance', shortArg: '' },
{ arg: 'strict-internal-urls', shortArg: '' },
{ arg: 'verbose', shortArg: '' },
{ arg: 'widevine', shortArg: '' },
])('test boolean arg %s', ({ arg, shortArg }) => {
const defaultArgs = parseArgs(
initArgs(['https://google.com']),
) as unknown as Record<string, boolean>;
expect(defaultArgs[arg]).toBe(false);
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`]),
) as unknown as Record<string, boolean>;
expect(args[arg]).toBe(true);
if (shortArg) {
const argsShort = parseArgs(
initArgs(['https://google.com', `-${shortArg}`]),
) as unknown as Record<string, boolean>;
expect(argsShort[arg]).toBe(true);
}
});
test.each([{ arg: 'no-overwrite', shortArg: '' }])(
'test inversible boolean arg %s',
({ arg, shortArg }) => {
const inverse = arg.startsWith('no-') ? arg.substr(3) : `no-${arg}`;
const defaultArgs = parseArgs(
initArgs(['https://google.com']),
) as unknown as Record<string, boolean>;
expect(defaultArgs[arg]).toBe(false);
expect(defaultArgs[inverse]).toBe(true);
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`]),
) as unknown as Record<string, boolean>;
expect(args[arg]).toBe(true);
expect(args[inverse]).toBe(false);
if (shortArg) {
const argsShort = parseArgs(
initArgs(['https://google.com', `-${shortArg}`]),
) as unknown as Record<string, boolean>;
expect(argsShort[arg]).toBe(true);
expect(argsShort[inverse]).toBe(true);
}
},
);
test.each([
{ arg: 'disk-cache-size', shortArg: '', value: 100 },
{ arg: 'height', shortArg: '', value: 200 },
{ arg: 'max-height', shortArg: '', value: 300 },
{ arg: 'max-width', shortArg: '', value: 400 },
{ arg: 'min-height', shortArg: '', value: 500 },
{ arg: 'min-width', shortArg: '', value: 600 },
{ arg: 'width', shortArg: '', value: 700 },
{ arg: 'x', shortArg: '', value: 800 },
{ arg: 'y', shortArg: '', value: 900 },
])('test numeric arg %s', ({ arg, shortArg, value }) => {
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`, `${value}`]),
) as unknown as Record<string, number>;
expect(args[arg]).toBe(value);
const badArgs = parseArgs(
initArgs(['https://google.com', `--${arg}`, 'abcd']),
) as unknown as Record<string, number>;
expect(badArgs[arg]).toBeNaN();
if (shortArg) {
const shortArgs = parseArgs(
initArgs(['https://google.com', `-${shortArg}`, `${value}`]),
) as unknown as Record<string, number>;
expect(shortArgs[arg]).toBe(value);
const badShortArgs = parseArgs(
initArgs(['https://google.com', `-${shortArg}`, 'abcd']),
) as unknown as Record<string, number>;
expect(badShortArgs[arg]).toBeNaN();
}
});
test.each([
{ arg: 'tray', value: 'true' },
{ arg: 'tray', value: 'false' },
{ arg: 'tray', value: 'start-in-tray' },
{ arg: 'tray', value: '' },
])('test tray valyue %s', ({ arg, value }) => {
const args = parseArgs(
initArgs(['https://google.com', `--${arg}`, `${value}`]),
) as unknown as Record<string, number>;
if (value !== '') {
expect(args[arg]).toBe(value);
} else {
expect(args[arg]).toBe('true');
}
});
test('test tray value defaults to false', () => {
const args = parseArgs(initArgs(['https://google.com']));
expect(args.tray).toBe('false');
});
});

View File

@ -1,83 +1,637 @@
#!/usr/bin/env node #!/usr/bin/env node
import 'source-map-support/register'; import 'source-map-support/register';
import * as commander from 'commander'; import electronPackager = require('electron-packager');
import * as dns from 'dns';
import * as log from 'loglevel'; import * as log from 'loglevel';
import yargs from 'yargs';
import { DEFAULT_ELECTRON_VERSION } from './constants';
import {
camelCased,
checkInternet,
getProcessEnvs,
isArgFormatInvalid,
} from './helpers/helpers';
import { supportedArchs, supportedPlatforms } from './infer/inferOs';
import { buildNativefierApp } from './main'; import { buildNativefierApp } from './main';
import { isArgFormatInvalid } from './helpers/helpers'; import { RawOptions } from '../shared/src/options/model';
import { isWindows } from './helpers/helpers'; import { parseJson } from './utils/parseUtils';
// package.json is `require`d to let tsc strip the `src` folder by determining // @types/yargs@17.x started pretending yargs.argv can be a promise:
// baseUrl=src. A static import would prevent that and cause an ugly extra "src" folder // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8e17f9ca957a06040badb53ae7688fbb74229ccf/types/yargs/index.d.ts#L73
const packageJson = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires // Dunno in which case it happens, but it doesn't for us! So, having to await
// (and end up having to flag sync code as async) would be useless and annoying.
// So, copy-pastaing and axing the Promise half of yargs's type definition,
// to have a *non*-promise type. Maybe that's wrong. If it is, this type should
// be dropped, and extra async-ness should be added where needed.
type YargsArgvSync<T> = {
[key in keyof yargs.Arguments<T> as
| key
| yargs.CamelCaseKey<key>]: yargs.Arguments<T>[key];
};
function collect(val: any, memo: any[]): any[] { export function initArgs(argv: string[]): yargs.Argv<RawOptions> {
memo.push(val); const sanitizedArgs = sanitizeArgs(argv);
return memo; const args = yargs(sanitizedArgs)
.scriptName('nativefier')
.usage(
'$0 <targetUrl> [outputDirectory] [other options]\nor\n$0 --upgrade <pathToExistingApp> [other options]',
)
.example(
'$0 <targetUrl> -n <name>',
'Make an app from <targetUrl> and set the application name to <name>',
)
.example(
'$0 --upgrade <pathToExistingApp>',
'Upgrade (in place) the existing Nativefier app at <pathToExistingApp>',
)
.example(
'$0 <targetUrl> -p <platform> -a <arch>',
'Make an app from <targetUrl> for the OS <platform> and CPU architecture <arch>',
)
.example(
'for more examples and help...',
'See https://github.com/nativefier/nativefier/blob/master/CATALOG.md',
)
.positional('targetUrl', {
description:
'the URL that you wish to to turn into a native app; required if not using --upgrade',
type: 'string',
})
.positional('outputDirectory', {
defaultDescription:
'defaults to the current directory, or env. var. NATIVEFIER_APPS_DIR if set',
description: 'the directory to generate the app in',
normalize: true,
type: 'string',
})
// App Creation Options
.option('a', {
alias: 'arch',
choices: supportedArchs,
defaultDescription: "current Node's arch",
description: 'the CPU architecture to build for',
type: 'string',
})
.option('c', {
alias: 'conceal',
default: false,
description: 'package the app source code into an asar archive',
type: 'boolean',
})
.option('e', {
alias: 'electron-version',
defaultDescription: DEFAULT_ELECTRON_VERSION,
description:
"specify the electron version to use (without the 'v'); see https://github.com/electron/electron/releases",
})
.option('global-shortcuts', {
description:
'define global keyboard shortcuts via a JSON file; See https://github.com/nativefier/nativefier/blob/master/API.md#global-shortcuts',
normalize: true,
type: 'string',
})
.option('i', {
alias: 'icon',
description:
'the icon file to use as the icon for the app (.ico on Windows, .icns/.png on macOS, .png on Linux)',
normalize: true,
type: 'string',
})
.option('n', {
alias: 'name',
defaultDescription: 'the title of the page passed via targetUrl',
description: 'specify the name of the app',
type: 'string',
})
.option('no-overwrite', {
default: false,
description: 'do not overwrite output directory if it already exists',
type: 'boolean',
})
.option('overwrite', {
// This is needed to have the `no-overwrite` flag to work correctly
default: true,
hidden: true,
type: 'boolean',
})
.option('p', {
alias: 'platform',
choices: supportedPlatforms,
defaultDescription: 'current operating system',
description: 'the operating system platform to build for',
type: 'string',
})
.option('portable', {
default: false,
description:
'make the app store its user data in the app folder; WARNING: see https://github.com/nativefier/nativefier/blob/master/API.md#portable for security risks',
type: 'boolean',
})
.option('upgrade', {
description:
'upgrade an app built by an older version of Nativefier\nYou must pass the full path to the existing app executable (app will be overwritten with upgraded version by default)',
normalize: true,
type: 'string',
})
.option('widevine', {
default: false,
description:
"use a Widevine-enabled version of Electron for DRM playback (use at your own risk, it's unofficial, provided by CastLabs)",
type: 'boolean',
})
.group(
[
'arch',
'conceal',
'electron-version',
'global-shortcuts',
'icon',
'name',
'no-overwrite',
'platform',
'portable',
'upgrade',
'widevine',
],
decorateYargOptionGroup('App Creation Options'),
)
// App Window Options
.option('always-on-top', {
default: false,
description: 'enable always on top window',
type: 'boolean',
})
.option('background-color', {
description:
"set the app background color, for better integration while the app is loading. Example value: '#2e2c29'",
type: 'string',
})
.option('bookmarks-menu', {
description:
'create a bookmarks menu (via JSON file); See https://github.com/nativefier/nativefier/blob/master/API.md#bookmarks-menu',
normalize: true,
type: 'string',
})
.option('browserwindow-options', {
coerce: parseJson,
description:
'override Electron BrowserWindow options (via JSON string); see https://github.com/nativefier/nativefier/blob/master/API.md#browserwindow-options',
})
.option('disable-context-menu', {
default: false,
description: 'disable the context menu (right click)',
type: 'boolean',
})
.option('disable-dev-tools', {
default: false,
description: 'disable developer tools (Ctrl+Shift+I / F12)',
type: 'boolean',
})
.option('full-screen', {
default: false,
description: 'always start the app full screen',
type: 'boolean',
})
.option('height', {
defaultDescription: '800',
description: 'set window default height in pixels',
type: 'number',
})
.option('hide-window-frame', {
default: false,
description: 'disable window frame and controls',
type: 'boolean',
})
.option('m', {
alias: 'show-menu-bar',
default: false,
description: 'set menu bar visible',
type: 'boolean',
})
.option('max-height', {
defaultDescription: 'unlimited',
description: 'set window maximum height in pixels',
type: 'number',
})
.option('max-width', {
defaultDescription: 'unlimited',
description: 'set window maximum width in pixels',
type: 'number',
})
.option('maximize', {
default: false,
description: 'always start the app maximized',
type: 'boolean',
})
.option('min-height', {
defaultDescription: '0',
description: 'set window minimum height in pixels',
type: 'number',
})
.option('min-width', {
defaultDescription: '0',
description: 'set window minimum width in pixels',
type: 'number',
})
.option('process-envs', {
coerce: getProcessEnvs,
description:
'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened',
})
.option('single-instance', {
default: false,
description: 'allow only a single instance of the app',
type: 'boolean',
})
.option('tray', {
default: 'false',
description:
"allow app to stay in system tray. If 'start-in-tray' is set as argument, don't show main window on first start",
choices: ['true', 'false', 'start-in-tray'],
})
.option('width', {
defaultDescription: '1280',
description: 'app window default width in pixels',
type: 'number',
})
.option('x', {
description: 'set window x location in pixels from left',
type: 'number',
})
.option('y', {
description: 'set window y location in pixels from top',
type: 'number',
})
.option('zoom', {
default: 1.0,
description: 'set the default zoom factor for the app',
type: 'number',
})
.group(
[
'always-on-top',
'background-color',
'bookmarks-menu',
'browserwindow-options',
'disable-context-menu',
'disable-dev-tools',
'full-screen',
'height',
'hide-window-frame',
'm',
'max-width',
'max-height',
'maximize',
'min-height',
'min-width',
'process-envs',
'single-instance',
'tray',
'width',
'x',
'y',
'zoom',
],
decorateYargOptionGroup('App Window Options'),
)
// Internal Browser Options
.option('file-download-options', {
coerce: parseJson,
description:
'a JSON string defining file download options; see https://github.com/sindresorhus/electron-dl',
})
.option('inject', {
description:
'path to a CSS/JS file to be injected; pass multiple times to inject multiple files',
string: true,
type: 'array',
})
.option('lang', {
defaultDescription: 'os language at runtime of the app',
description:
'set the language or locale to render the web site as (e.g., "fr", "en-US", "es", etc.)',
type: 'string',
})
.option('u', {
alias: 'user-agent',
description:
"set the app's user agent string; may also use 'edge', 'firefox', or 'safari' to have one auto-generated",
type: 'string',
})
.option('user-agent-honest', {
alias: 'honest',
default: false,
description:
'prevent the normal changing of the user agent string to appear as a regular Chrome browser',
type: 'boolean',
})
.group(
[
'file-download-options',
'inject',
'lang',
'user-agent',
'user-agent-honest',
],
decorateYargOptionGroup('Internal Browser Options'),
)
// Internal Browser Cache Options
.option('clear-cache', {
default: false,
description: 'prevent the app from preserving cache between launches',
type: 'boolean',
})
.option('disk-cache-size', {
defaultDescription: 'chromium default',
description:
'set the maximum disk space (in bytes) to be used by the disk cache',
type: 'number',
})
.group(
['clear-cache', 'disk-cache-size'],
decorateYargOptionGroup('Internal Browser Cache Options'),
)
// URL Handling Options
.option('block-external-urls', {
default: false,
description: `forbid navigation to URLs not considered "internal" (see '--internal-urls'). Instead of opening in an external browser, attempts to navigate to external URLs will be blocked`,
type: 'boolean',
})
.option('internal-urls', {
defaultDescription: 'URLs sharing the same base domain',
description: `regex of URLs to consider "internal"; by default matches based on domain (see '--strict-internal-urls'); all other URLs will be opened in an external browser`,
type: 'string',
})
.option('strict-internal-urls', {
default: false,
description: 'disable domain-based matching on internal URLs',
type: 'boolean',
})
.option('proxy-rules', {
description:
'proxy rules; see https://www.electronjs.org/docs/api/session#sessetproxyconfig',
type: 'string',
})
.group(
[
'block-external-urls',
'internal-urls',
'strict-internal-urls',
'proxy-rules',
],
decorateYargOptionGroup('URL Handling Options'),
)
// Auth Options
.option('basic-auth-password', {
description: 'basic http(s) auth password',
type: 'string',
})
.option('basic-auth-username', {
description: 'basic http(s) auth username',
type: 'string',
})
.group(
['basic-auth-password', 'basic-auth-username'],
decorateYargOptionGroup('Auth Options'),
)
// Graphics Options
.option('disable-gpu', {
default: false,
description: 'disable hardware acceleration',
type: 'boolean',
})
.option('enable-es3-apis', {
default: false,
description: 'force activation of WebGL 2.0',
type: 'boolean',
})
.option('ignore-gpu-blacklist', {
default: false,
description: 'force WebGL apps to work on unsupported GPUs',
type: 'boolean',
})
.group(
['disable-gpu', 'enable-es3-apis', 'ignore-gpu-blacklist'],
decorateYargOptionGroup('Graphics Options'),
)
// (In)Security Options
.option('disable-old-build-warning-yesiknowitisinsecure', {
default: false,
description:
'disable warning shown when opening an app made too long ago; Nativefier uses the Chrome browser (through Electron), and it is dangerous to keep using an old version of it',
type: 'boolean',
})
.option('ignore-certificate', {
default: false,
description: 'ignore certificate-related errors',
type: 'boolean',
})
.option('insecure', {
default: false,
description: 'enable loading of insecure content',
type: 'boolean',
})
.group(
[
'disable-old-build-warning-yesiknowitisinsecure',
'ignore-certificate',
'insecure',
],
decorateYargOptionGroup('(In)Security Options'),
)
// Flash Options (DEPRECATED)
.option('flash', {
default: false,
deprecated: true,
description: 'enable Adobe Flash',
hidden: true,
type: 'boolean',
})
.option('flash-path', {
deprecated: true,
description: 'path to Chrome flash plugin; find it in `chrome://plugins`',
hidden: true,
normalize: true,
type: 'string',
})
// Platform Specific Options
.option('app-copyright', {
description:
'(macOS, windows only) set a human-readable copyright line for the app; maps to `LegalCopyright` metadata property on Windows, and `NSHumanReadableCopyright` on macOS',
type: 'string',
})
.option('app-version', {
description:
'(macOS, windows only) set the version of the app; maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on macOS',
type: 'string',
})
.option('bounce', {
default: false,
description:
'(macOS only) make the dock icon bounce when the counter increases',
type: 'boolean',
})
.option('build-version', {
description:
'(macOS, windows only) set the build version of the app; maps to `FileVersion` metadata property on Windows, and `CFBundleVersion` on macOS',
type: 'string',
})
.option('counter', {
default: false,
description:
'(macOS only) set a dock count badge, determined by looking for a number in the window title',
type: 'boolean',
})
.option('darwin-dark-mode-support', {
default: false,
description: '(macOS only) enable Dark Mode support on macOS 10.14+',
type: 'boolean',
})
.option('f', {
alias: 'fast-quit',
default: false,
description: '(macOS only) quit app on window close',
type: 'boolean',
})
.option('title-bar-style', {
choices: ['hidden', 'hiddenInset'],
description:
'(macOS only) set title bar style; consider injecting custom CSS (via --inject) for better integration',
type: 'string',
})
.option('win32metadata', {
coerce: (value: string) =>
parseJson<electronPackager.Win32MetadataOptions>(value),
description:
'(windows only) a JSON string of key/value pairs (ProductName, InternalName, FileDescription) to embed as executable metadata',
})
.group(
[
'app-copyright',
'app-version',
'bounce',
'build-version',
'counter',
'darwin-dark-mode-support',
'fast-quit',
'title-bar-style',
'win32metadata',
],
decorateYargOptionGroup('Platform-Specific Options'),
)
// Debug Options
.option('crash-reporter', {
description: 'remote server URL to send crash reports',
type: 'string',
})
.option('verbose', {
default: false,
description: 'enable verbose/debug/troubleshooting logs',
type: 'boolean',
})
.option('quiet', {
default: false,
description: 'suppress all logging',
type: 'boolean',
})
.group(
['crash-reporter', 'verbose', 'quiet'],
decorateYargOptionGroup('Debug Options'),
)
.version()
.help()
.group(['version', 'help'], 'Other Options')
.wrap(yargs.terminalWidth());
// We must access argv in order to get yargs to actually process args
// Do this now to go ahead and get any errors out of the way
args.argv as YargsArgvSync<RawOptions>;
return args as yargs.Argv<RawOptions>;
} }
function parseBooleanOrString(val: string): boolean | string { function decorateYargOptionGroup(value: string): string {
switch (val) { return `====== ${value} ======`;
case 'true':
return true;
case 'false':
return false;
default:
return val;
}
} }
function parseJson(val: string): any { export function parseArgs(args: yargs.Argv<RawOptions>): RawOptions {
if (!val) return {}; const parsed = { ...(args.argv as YargsArgvSync<RawOptions>) };
// In yargs, the _ property of the parsed args is an array of the positional args
// https://github.com/yargs/yargs/blob/master/docs/examples.md#and-non-hyphenated-options-too-just-use-argv_
// So try to extract the targetUrl and outputDirectory from these
parsed.targetUrl = parsed._.length > 0 ? parsed._[0].toString() : undefined;
parsed.out = parsed._.length > 1 ? (parsed._[1] as string) : undefined;
if (parsed.upgrade && parsed.targetUrl) {
let targetAndUpgrade = false;
if (!parsed.out) {
// If we're upgrading, the first positional args might be the outputDirectory, so swap these if we can
try { try {
return JSON.parse(val); // If this succeeds, we have a problem
} catch (err) { new URL(parsed.targetUrl);
const windowsShellHint = isWindows() targetAndUpgrade = true;
? `\n In particular, Windows cmd doesn't have single quotes, so you have to use only double-quotes plus escaping: "{\\"someKey\\": \\"someValue\\"}"` } catch {
: ''; // Cool, it's not a URL
parsed.out = parsed.targetUrl;
log.error( parsed.targetUrl = undefined;
`Unable to parse JSON value: ${val}\n` +
`JSON should look like {"someString": "someValue", "someBoolean": true, "someArray": [1,2,3]}.\n` +
` - Only double quotes are allowed, single quotes are not.\n` +
` - Learn how your shell behaves and escapes characters.${windowsShellHint}\n` +
` - If unsure, validate your JSON using an online service.`,
);
throw err;
} }
} else {
// Someone supplied a targetUrl, an outputDirectory, and --upgrade. That's not cool.
targetAndUpgrade = true;
} }
function getProcessEnvs(val: string): any { if (targetAndUpgrade) {
if (!val) { throw new Error(
return {}; 'ERROR: Nativefier must be called with either a targetUrl or the --upgrade option, not both.\n',
}
return parseJson(val);
}
function checkInternet(): void {
dns.lookup('npmjs.com', (err) => {
if (err && err.code === 'ENOTFOUND') {
log.warn(
'\nNo Internet Connection\nTo offline build, download electron from https://github.com/electron/electron/releases\nand place in ~/AppData/Local/electron/Cache/ on Windows,\n~/.cache/electron on Linux or ~/Library/Caches/electron/ on Mac\nUse --electron-version to specify the version you downloaded.',
); );
} }
});
} }
if (require.main === module) { if (!parsed.targetUrl && !parsed.upgrade) {
const sanitizedArgs = []; throw new Error(
process.argv.forEach((arg) => { 'ERROR: Nativefier must be called with either a targetUrl or the --upgrade option.\n',
);
}
parsed.noOverwrite = parsed['no-overwrite'] = !parsed.overwrite;
// Since coerce in yargs seems to have broken since
// https://github.com/yargs/yargs/pull/1978
for (const arg of [
'win32metadata',
'browserwindow-options',
'file-download-options',
]) {
if (parsed[arg] && typeof parsed[arg] === 'string') {
parsed[arg] = parseJson(parsed[arg] as string);
// sets fileDownloadOptions and browserWindowOptions
// as parsed object as they were still strings in `nativefier.json`
// because only their snake-cased variants were being parsed above
parsed[camelCased(arg)] = parsed[arg];
}
}
if (parsed['process-envs'] && typeof parsed['process-envs'] === 'string') {
parsed['process-envs'] = getProcessEnvs(parsed['process-envs']);
}
return parsed;
}
function sanitizeArgs(argv: string[]): string[] {
const sanitizedArgs: string[] = [];
argv.forEach((arg) => {
if (isArgFormatInvalid(arg)) { if (isArgFormatInvalid(arg)) {
log.error( throw new Error(
`Invalid argument passed: ${arg} .\nNativefier supports short options (like "-n") and long options (like "--name"), all lowercase. Run "nativefier --help" for help.\nAborting`, `Invalid argument passed: ${arg} .\nNativefier supports short options (like "-n") and long options (like "--name"), all lowercase. Run "nativefier --help" for help.\nAborting`,
); );
process.exit(1);
} }
const isLastArg = sanitizedArgs.length + 1 === argv.length;
if (sanitizedArgs.length > 0) { if (sanitizedArgs.length > 0) {
const previousArg = sanitizedArgs[sanitizedArgs.length - 1]; const previousArg = sanitizedArgs[sanitizedArgs.length - 1];
log.debug({ arg, previousArg, isLastArg });
// Work around commander.js not supporting default argument for options // Work around commander.js not supporting default argument for options
if ( if (
previousArg === '--tray' && previousArg === '--tray' &&
@ -87,230 +641,64 @@ if (require.main === module) {
} }
} }
sanitizedArgs.push(arg); sanitizedArgs.push(arg);
if (arg === '--tray' && isLastArg) {
// Add a true if --tray is last so it gets enabled
sanitizedArgs.push('true');
}
}); });
const positionalOptions = { return sanitizedArgs;
targetUrl: '', }
out: '',
};
const args = commander
.name('nativefier')
.version(packageJson.version, '-v, --version')
.arguments('<targetUrl> [dest]')
.action((url, outputDirectory) => {
positionalOptions.targetUrl = url;
positionalOptions.out = outputDirectory;
})
.option('-n, --name <value>', 'app name')
.option('-p, --platform <value>', "'mac', 'mas', 'linux' or 'windows'")
.option('-a, --arch <value>', "'ia32' or 'x64' or 'arm' or 'arm64'")
.option(
'--app-version <value>',
'(macOS, windows only) the version of the app. Maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on macOS.',
)
.option(
'--build-version <value>',
'(macOS, windows only) The build version of the app. Maps to `FileVersion` metadata property on Windows, and `CFBundleVersion` on macOS',
)
.option(
'--app-copyright <value>',
'(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>',
'(windows only) a JSON string of key/value pairs (ProductName, InternalName, FileDescription) to embed as executable metadata',
parseJson,
)
.option(
'-e, --electron-version <value>',
"electron version to package, without the 'v', see https://github.com/electron/electron/releases",
)
.option(
'--widevine',
"use a Widevine-enabled version of Electron for DRM playback (use at your own risk, it's unofficial, provided by CastLabs)",
)
.option(
'--no-overwrite',
'do not override output directory if it already exists; defaults to false',
)
.option(
'-c, --conceal',
'packages the app source code into an asar archive; defaults to false',
)
.option(
'--counter',
'(macOS only) set a dock count badge, determined by looking for a number in the window title; defaults to false',
)
.option(
'--bounce',
'(macOS only) make the dock icon bounce when the counter increases; defaults to false',
)
.option(
'-i, --icon <value>',
'the icon file to use as the icon for the app (should be a .png, on macOS can also be an .icns)',
)
.option(
'--width <value>',
'set window default width; defaults to 1280px',
parseInt,
)
.option(
'--height <value>',
'set window default height; defaults to 800px',
parseInt,
)
.option(
'--min-width <value>',
'set window minimum width; defaults to 0px',
parseInt,
)
.option(
'--min-height <value>',
'set window minimum height; defaults to 0px',
parseInt,
)
.option(
'--max-width <value>',
'set window maximum width; default is unlimited',
parseInt,
)
.option(
'--max-height <value>',
'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(
'-f, --fast-quit',
'(macOS only) quit app on window close; defaults to false',
)
.option('-u, --user-agent <value>', 'set the app user agent string')
.option(
'--honest',
'prevent the normal changing of the user agent string to appear as a regular Chrome browser',
)
.option('--ignore-certificate', 'ignore certificate-related errors')
.option('--disable-gpu', 'disable hardware acceleration')
.option(
'--ignore-gpu-blacklist',
'force WebGL apps to work on unsupported GPUs',
)
.option('--enable-es3-apis', 'force activation of WebGL 2.0')
.option(
'--insecure',
'enable loading of insecure content; defaults to false',
)
.option('--flash', 'enables Adobe Flash; defaults to false')
.option(
'--flash-path <value>',
'path to Chrome flash plugin; find it in `chrome://plugins`',
)
.option(
'--disk-cache-size <value>',
'forces the maximum disk space (in bytes) to be used by the disk cache',
)
.option(
'--inject <value>',
'path to a CSS/JS file to be injected. Pass multiple times to inject multiple files.',
collect,
[],
)
.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', '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',
parseFloat,
)
.option(
'--internal-urls <value>',
'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(
'--block-external-urls',
`forbid navigation to URLs not considered "internal" (see '--internal-urls'). Instead of opening in an external browser, attempts to navigate to external URLs will be blocked. Default: false`,
)
.option(
'--proxy-rules <value>',
'proxy rules; see https://www.electronjs.org/docs/api/session#sessetproxyconfig',
)
.option(
'--crash-reporter <value>',
'remote server URL to send crash reports',
)
.option(
'--single-instance',
'allow only a single instance of the application',
)
.option(
'--clear-cache',
'prevent the application from preserving cache between launches',
)
.option(
'--processEnvs <json-string>',
'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened',
getProcessEnvs,
)
.option(
'--file-download-options <json-string>',
'a JSON string of key/value pairs to be set as file download options. See https://github.com/sindresorhus/electron-dl for available options.',
parseJson,
)
.option(
'--tray [start-in-tray]',
"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",
)
.option(
'--global-shortcuts <value>',
'JSON file defining global shortcuts. See https://github.com/jiahaog/nativefier/blob/master/docs/api.md#global-shortcuts',
)
.option(
'--browserwindow-options <json-string>',
'a JSON string that will be sent directly into electron BrowserWindow options. See https://github.com/jiahaog/nativefier/blob/master/docs/api.md#browserwindow-options',
parseJson,
)
.option(
'--background-color <value>',
"sets the app background color, for better integration while the app is loading. Example value: '#2e2c29'",
)
.option(
'--disable-old-build-warning-yesiknowitisinsecure',
'Disables warning when opening an app made with an old version of Nativefier. Nativefier uses the Chrome browser (through Electron), and it is dangerous to keep using an old version of it.)',
)
.option(
'--darwin-dark-mode-support',
'(macOS only) enable Dark Mode support on macOS 10.14+',
);
if (require.main === module) {
let args: yargs.Argv<RawOptions> | undefined = undefined;
let parsedArgs: RawOptions;
try { try {
args.parse(sanitizedArgs); args = initArgs(process.argv.slice(2));
} catch (err) { parsedArgs = parseArgs(args);
log.error('Failed to parse command-line arguments. Aborting.'); } catch (err: unknown) {
if (args) {
log.error(err);
args.showHelp();
} else {
log.error('Failed to parse command-line arguments. Aborting.', err);
}
process.exit(1); process.exit(1);
} }
if (!process.argv.slice(2).length) { const options: RawOptions = {
commander.help(); ...parsedArgs,
};
if (options.verbose) {
log.setLevel('trace');
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
require('debug').enable('electron-packager');
} catch (err: unknown) {
log.debug(
'Failed to enable electron-packager debug output. This should not happen,',
'and suggests their internals changed. Please report an issue.',
);
} }
log.debug(
'Running in verbose mode! This will produce a mountain of logs and',
'is recommended only for troubleshooting or if you like Shakespeare.',
);
} else if (options.quiet) {
log.setLevel('silent');
} else {
log.setLevel('info');
}
checkInternet(); checkInternet();
const options = { ...positionalOptions, ...commander.opts() };
if (!options.out && process.env.NATIVEFIER_APPS_DIR) {
options.out = process.env.NATIVEFIER_APPS_DIR;
}
buildNativefierApp(options).catch((error) => { buildNativefierApp(options).catch((error) => {
log.error('Error during build. Run with --verbose for details.', error); log.error('Error during build. Run with --verbose for details.', error);
}); });

View File

@ -2,9 +2,25 @@ import * as path from 'path';
export const DEFAULT_APP_NAME = 'APP'; export const DEFAULT_APP_NAME = 'APP';
// Update both together // Upgrade both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together, and
export const DEFAULT_ELECTRON_VERSION = '11.0.3'; // - upgrade app / package.json / "devDependencies" / "electron"
export const DEFAULT_CHROME_VERSION = '87.0.4280.67'; // - upgrade package.json / "devDependencies" / "electron"
// Doing a *major* upgrade? Read https://github.com/nativefier/nativefier/blob/master/HACKING.md#deps-major-upgrading-electron
export const DEFAULT_ELECTRON_VERSION = '25.7.0';
// https://atom.io/download/atom-shell/index.json
// https://www.electronjs.org/releases/stable
export const DEFAULT_CHROME_VERSION = '114.0.5735.289';
// Update each of these periodically
// https://product-details.mozilla.org/1.0/firefox_versions.json
export const DEFAULT_FIREFOX_VERSION = '116.0.3';
// https://en.wikipedia.org/wiki/Safari_version_history
export const DEFAULT_SAFARI_VERSION = {
majorVersion: 16,
version: '16.6',
webkitVersion: '605.1.15',
};
export const ELECTRON_MAJOR_VERSION = parseInt( export const ELECTRON_MAJOR_VERSION = parseInt(
DEFAULT_ELECTRON_VERSION.split('.')[0], DEFAULT_ELECTRON_VERSION.split('.')[0],

19
src/helpers/fsHelpers.ts Normal file
View File

@ -0,0 +1,19 @@
import * as fs from 'fs';
export function dirExists(dirName: string): boolean {
try {
const dirStat = fs.statSync(dirName);
return dirStat.isDirectory();
} catch {
return false;
}
}
export function fileExists(fileName: string): boolean {
try {
const fileStat = fs.statSync(fileName);
return fileStat.isFile();
} catch {
return false;
}
}

View File

@ -1,4 +1,8 @@
import { isArgFormatInvalid } from './helpers'; import {
isArgFormatInvalid,
generateRandomSuffix,
camelCased,
} from './helpers';
describe('isArgFormatInvalid', () => { describe('isArgFormatInvalid', () => {
test('is false for correct short args', () => { test('is false for correct short args', () => {
@ -9,6 +13,11 @@ describe('isArgFormatInvalid', () => {
expect(isArgFormatInvalid('--t')).toBe(true); expect(isArgFormatInvalid('--t')).toBe(true);
}); });
test('is false for --x and --y (backwards compat, we should have made these short, oh well)', () => {
expect(isArgFormatInvalid('--x')).toBe(false);
expect(isArgFormatInvalid('--y')).toBe(false);
});
test('is false for correct long args', () => { test('is false for correct long args', () => {
expect(isArgFormatInvalid('--test')).toBe(false); expect(isArgFormatInvalid('--test')).toBe(false);
}); });
@ -29,3 +38,47 @@ describe('isArgFormatInvalid', () => {
expect(isArgFormatInvalid('--test-run-with-many-dashes')).toBe(false); expect(isArgFormatInvalid('--test-run-with-many-dashes')).toBe(false);
}); });
}); });
describe('generateRandomSuffix', () => {
test('is not empty', () => {
expect(generateRandomSuffix()).not.toBe('');
});
test('is not null', () => {
expect(generateRandomSuffix()).not.toBeNull();
});
test('is not undefined', () => {
expect(generateRandomSuffix()).toBeDefined();
});
test('is different per call', () => {
expect(generateRandomSuffix()).not.toBe(generateRandomSuffix());
});
test('respects the length param', () => {
expect(generateRandomSuffix(10).length).toBe(10);
});
});
describe('camelCased', () => {
test('has no hyphens in camel case', () => {
expect(camelCased('file-download')).toEqual(expect.not.stringMatching(/-/));
});
test('returns camel cased string', () => {
expect(camelCased('file-download')).toBe('fileDownload');
});
test('has no spaces in camel case', () => {
expect(camelCased('--file--download--')).toBe('fileDownload');
});
test('handles multiple hyphens properly', () => {
expect(camelCased('file--download--options')).toBe('fileDownloadOptions');
});
test('does not affect non-snake cased strings', () => {
expect(camelCased('win32options')).toBe('win32options');
});
});

View File

@ -1,21 +1,43 @@
import { spawnSync } from 'child_process';
import * as crypto from 'crypto';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import axios from 'axios'; import axios from 'axios';
import * as dns from 'dns';
import * as hasbin from 'hasbin'; import * as hasbin from 'hasbin';
import { ncp } from 'ncp';
import * as log from 'loglevel'; import * as log from 'loglevel';
import * as tmp from 'tmp'; import * as tmp from 'tmp';
import { parseJson } from '../utils/parseUtils';
tmp.setGracefulCleanup(); // cleanup temp dirs even when an uncaught exception occurs tmp.setGracefulCleanup(); // cleanup temp dirs even when an uncaught exception occurs
const now = new Date(); const now = new Date();
const TMP_TIME = `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`; const TMP_TIME = `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`;
type DownloadResult = { export type DownloadResult = {
data: Buffer; data: Buffer;
ext: string; ext: string;
}; };
type ProcessEnvs = Record<string, unknown>;
export function hasWine(): boolean {
return hasbin.sync('wine');
}
// I tried to place this (and the other is* functions) in
// a new shared helpers, but alas eslint gets real confused
// about the type signatures and thinks they're all any.
// TODO: Figure out a way to refactor duplicate code from
// src/helpers/helpers.ts and app/src/helpers/helpers.ts
// into the shared module
export function isLinux(): boolean {
return os.platform() === 'linux';
}
export function isOSX(): boolean { export function isOSX(): boolean {
return os.platform() === 'darwin'; return os.platform() === 'darwin';
} }
@ -24,6 +46,16 @@ export function isWindows(): boolean {
return os.platform() === 'win32'; return os.platform() === 'win32';
} }
export function isWindowsAdmin(): boolean {
if (process.platform !== 'win32') {
return false;
}
// https://stackoverflow.com/questions/4051883/batch-script-how-to-check-for-admin-rights
// https://stackoverflow.com/questions/57009374/check-admin-or-non-admin-users-in-nodejs-or-javascript
return spawnSync('fltmc').status === 0;
}
/** /**
* Create a temp directory with a debug-friendly name, and return its path. * Create a temp directory with a debug-friendly name, and return its path.
* Will be automatically deleted on exit. * Will be automatically deleted on exit.
@ -36,29 +68,17 @@ export function getTempDir(prefix: string, mode?: number): string {
}).name; }).name;
} }
export async function copyFileOrDir( export function downloadFile(
sourceFileOrDir: string, fileUrl: string,
dest: string, ): Promise<DownloadResult | undefined> {
): Promise<void> {
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}`); log.debug(`Downloading ${fileUrl}`);
return axios return axios
.get(fileUrl, { .get<Buffer>(fileUrl, {
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {
if (!response.data) { if (!response.data) {
return null; return undefined;
} }
return { return {
data: response.data, data: response.data,
@ -68,8 +88,8 @@ export async function downloadFile(fileUrl: string): Promise<DownloadResult> {
} }
export function getAllowedIconFormats(platform: string): string[] { export function getAllowedIconFormats(platform: string): string[] {
const hasIdentify = hasbin.sync('identify'); const hasIdentify = hasbin.sync('identify') || hasbin.sync('gm');
const hasConvert = hasbin.sync('convert'); const hasConvert = hasbin.sync('convert') || hasbin.sync('gm');
const hasIconUtil = hasbin.sync('iconutil'); const hasIconUtil = hasbin.sync('iconutil');
const pngToIcns = hasConvert && hasIconUtil; const pngToIcns = hasConvert && hasIconUtil;
@ -81,7 +101,7 @@ export function getAllowedIconFormats(platform: string): string[] {
const icnsToPng = false; const icnsToPng = false;
const icnsToIco = false; const icnsToIco = false;
const formats = []; const formats: string[] = [];
// Shell scripting is not supported on windows, temporary override // Shell scripting is not supported on windows, temporary override
if (isWindows()) { if (isWindows()) {
@ -145,8 +165,47 @@ export function getAllowedIconFormats(platform: string): string[] {
*/ */
export function isArgFormatInvalid(arg: string): boolean { export function isArgFormatInvalid(arg: string): boolean {
return ( return (
arg.startsWith('---') || (arg.startsWith('---') ||
/^--[a-z]$/i.exec(arg) !== null || /^--[a-z]$/i.exec(arg) !== null ||
/^-[a-z]{2,}$/i.exec(arg) !== null /^-[a-z]{2,}$/i.exec(arg) !== null) &&
!['--x', '--y'].includes(arg) // exception for long args --{x,y}
); );
} }
export function generateRandomSuffix(length = 6): string {
const hash = crypto.createHash('md5');
// Add a random salt to help avoid collisions
hash.update(crypto.randomBytes(256));
return hash.digest('hex').substring(0, length);
}
export function getProcessEnvs(val: string): ProcessEnvs | undefined {
if (!val) {
return undefined;
}
return parseJson<ProcessEnvs>(val);
}
export function checkInternet(): void {
dns.lookup('npmjs.com', (err) => {
if (err && err.code === 'ENOTFOUND') {
log.warn(
'\nNo Internet Connection\nTo offline build, download electron from https://github.com/electron/electron/releases\nand place in ~/AppData/Local/electron/Cache/ on Windows,\n~/.cache/electron on Linux or ~/Library/Caches/electron/ on Mac\nUse --electron-version to specify the version you downloaded.',
);
}
});
}
/**
* Takes in a snake-cased string and converts to camelCase
*/
export function camelCased(str: string): string {
return str
.split('-')
.filter((s) => s.length > 0)
.map((word, i) => {
if (i === 0) return word;
return `${word[0].toUpperCase()}${word.substring(1)}`;
})
.join('');
}

View File

@ -1,6 +1,5 @@
import * as path from 'path'; import * as path from 'path';
import { spawnSync } from 'child_process';
import * as shell from 'shelljs';
import { isWindows, isOSX, getTempDir } from './helpers'; import { isWindows, isOSX, getTempDir } from './helpers';
import * as log from 'loglevel'; import * as log from 'loglevel';
@ -10,49 +9,49 @@ const SCRIPT_PATHS = {
convertToPng: path.join(__dirname, '../..', 'icon-scripts/convertToPng'), convertToPng: path.join(__dirname, '../..', 'icon-scripts/convertToPng'),
convertToIco: path.join(__dirname, '../..', 'icon-scripts/convertToIco'), convertToIco: path.join(__dirname, '../..', 'icon-scripts/convertToIco'),
convertToIcns: path.join(__dirname, '../..', 'icon-scripts/convertToIcns'), convertToIcns: path.join(__dirname, '../..', 'icon-scripts/convertToIcns'),
convertToTrayIcon: path.join(
__dirname,
'../..',
'icon-scripts/convertToTrayIcon',
),
}; };
/** /**
* Executes a shell script with the form "./pathToScript param1 param2" * Executes a shell script with the form "./pathToScript param1 param2"
*/ */
async function iconShellHelper( function iconShellHelper(
shellScriptPath: string, shellScriptPath: string,
icoSource: string, icoSource: string,
icoDestination: string, icoDestination: string,
): Promise<string> { ): string {
return new Promise((resolve, reject) => {
if (isWindows()) { if (isWindows()) {
reject( throw new Error(
new Error(
'Icon conversion only supported on macOS or Linux. ' + '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 Windows, download/create a .ico and pass it with --icon favicon.ico . ' +
'If building for macOS/Linux, do it from macOS/Linux', 'If building for macOS/Linux, do it from macOS/Linux',
),
); );
return;
} }
const shellCommand = `"${shellScriptPath}" "${icoSource}" "${icoDestination}"`; const shellCommand = `"${shellScriptPath}" "${icoSource}" "${icoDestination}"`;
log.debug( log.debug(
`Converting icon ${icoSource} to ${icoDestination}.`, `Converting icon ${icoSource} to ${icoDestination}.`,
`Calling: ${shellCommand}`, `Calling shell command: ${shellCommand}`,
);
const { stdout, stderr, status } = spawnSync(
shellScriptPath,
[icoSource, icoDestination],
{ timeout: 10000 },
);
if (status) {
throw new Error(
`Icon conversion failed with status code ${status}.\nstdout: ${stdout.toString()}\nstderr: ${stderr.toString()}`,
); );
shell.exec(shellCommand, { silent: true }, (exitCode, stdOut, stdError) => {
if (exitCode) {
reject({
stdOut,
stdError,
});
return;
} }
log.debug(`Conversion succeeded and produced icon at ${icoDestination}`); log.debug(`Conversion succeeded and produced icon at ${icoDestination}`);
resolve(icoDestination); return icoDestination;
});
});
} }
export function singleIco(icoSrc: string): Promise<string> { export function singleIco(icoSrc: string): string {
return iconShellHelper( return iconShellHelper(
SCRIPT_PATHS.singleIco, SCRIPT_PATHS.singleIco,
icoSrc, icoSrc,
@ -60,7 +59,7 @@ export function singleIco(icoSrc: string): Promise<string> {
); );
} }
export async function convertToPng(icoSrc: string): Promise<string> { export function convertToPng(icoSrc: string): string {
return iconShellHelper( return iconShellHelper(
SCRIPT_PATHS.convertToPng, SCRIPT_PATHS.convertToPng,
icoSrc, icoSrc,
@ -68,7 +67,7 @@ export async function convertToPng(icoSrc: string): Promise<string> {
); );
} }
export async function convertToIco(icoSrc: string): Promise<string> { export function convertToIco(icoSrc: string): string {
return iconShellHelper( return iconShellHelper(
SCRIPT_PATHS.convertToIco, SCRIPT_PATHS.convertToIco,
icoSrc, icoSrc,
@ -76,7 +75,7 @@ export async function convertToIco(icoSrc: string): Promise<string> {
); );
} }
export async function convertToIcns(icoSrc: string): Promise<string> { export function convertToIcns(icoSrc: string): string {
if (!isOSX()) { if (!isOSX()) {
throw new Error('macOS is required to convert to a .icns icon'); throw new Error('macOS is required to convert to a .icns icon');
} }
@ -87,3 +86,15 @@ export async function convertToIcns(icoSrc: string): Promise<string> {
`${getTempDir('iconconv')}/icon.icns`, `${getTempDir('iconconv')}/icon.icns`,
); );
} }
export function convertToTrayIcon(icoSrc: string): string {
if (!isOSX()) {
throw new Error('macOS is required to convert from a .icns icon');
}
return iconShellHelper(
SCRIPT_PATHS.convertToTrayIcon,
icoSrc,
`${path.dirname(icoSrc)}/tray-icon.png`,
);
}

View File

@ -0,0 +1,208 @@
import * as fs from 'fs';
import * as path from 'path';
import * as log from 'loglevel';
import { NativefierOptions } from '../../../shared/src/options/model';
import { getVersionString } from './rceditGet';
import { fileExists } from '../fsHelpers';
type ExecutableInfo = {
arch?: string;
};
function getExecutableBytes(executablePath: string): Uint8Array {
return fs.readFileSync(executablePath);
}
function getExecutableArch(
exeBytes: Uint8Array,
platform: string,
): string | undefined {
switch (platform) {
case 'linux':
// https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
switch (exeBytes[0x12]) {
case 0x03:
return 'ia32';
case 0x28:
return 'armv7l';
case 0x3e:
return 'x64';
case 0xb7:
return 'arm64';
default:
return undefined;
}
case 'darwin':
case 'mas':
// https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
switch ((exeBytes[0x04] << 8) + exeBytes[0x05]) {
case 0x0700:
return 'x64';
case 0x0c00:
return 'arm64';
default:
return undefined;
}
case 'win32':
// https://en.wikibooks.org/wiki/X86_Disassembly/Windows_Executable_Files#COFF_Header
switch ((exeBytes[0x7d] << 8) + exeBytes[0x7c]) {
case 0x014c:
return 'ia32';
case 0x8664:
return 'x64';
case 0xaa64:
return 'arm64';
default:
return undefined;
}
default:
return undefined;
}
}
function getExecutableInfo(
executablePath: string,
platform: string,
): ExecutableInfo | undefined {
if (!fileExists(executablePath)) {
return undefined;
}
const exeBytes = getExecutableBytes(executablePath);
return {
arch: getExecutableArch(exeBytes, platform),
};
}
export function getOptionsFromExecutable(
appResourcesDir: string,
priorOptions: NativefierOptions,
): NativefierOptions {
const newOptions: NativefierOptions = { ...priorOptions };
if (!newOptions.name) {
throw new Error(
'Can not extract options from executable with no name specified.',
);
}
const name: string = newOptions.name;
let executablePath: string | undefined = undefined;
const appRoot = path.resolve(path.join(appResourcesDir, '..', '..'));
const children = fs.readdirSync(appRoot, { withFileTypes: true });
const looksLikeMacOS =
children.filter((c) => c.name === 'MacOS' && c.isDirectory()).length > 0;
const looksLikeWindows =
children.filter((c) => c.name.toLowerCase().endsWith('.exe') && c.isFile())
.length > 0;
const looksLikeLinux =
children.filter((c) => c.name.toLowerCase().endsWith('.so') && c.isFile())
.length > 0;
if (looksLikeMacOS) {
log.debug('This looks like a MacOS app...');
if (newOptions.platform === undefined) {
newOptions.platform =
children.filter((c) => c.name === 'Library' && c.isDirectory()).length >
0
? 'mas'
: 'darwin';
}
executablePath = path.join(
appRoot,
'MacOS',
fs.readdirSync(path.join(appRoot, 'MacOS'))[0],
);
} else if (looksLikeWindows) {
log.debug('This looks like a Windows app...');
if (newOptions.platform === undefined) {
newOptions.platform = 'win32';
}
executablePath = path.join(
appRoot,
children.filter(
(c) =>
c.name.toLowerCase() === `${name.toLowerCase()}.exe` && c.isFile(),
)[0].name,
);
if (newOptions.appVersion === undefined) {
// https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L46-L48
newOptions.appVersion = getVersionString(
executablePath,
'ProductVersion',
);
log.debug(
`Extracted app version from executable: ${
newOptions.appVersion as string
}`,
);
}
if (newOptions.buildVersion === undefined) {
//https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L50-L52
newOptions.buildVersion = getVersionString(executablePath, 'FileVersion');
if (newOptions.appVersion == newOptions.buildVersion) {
newOptions.buildVersion = undefined;
} else {
log.debug(
`Extracted build version from executable: ${
newOptions.buildVersion as string
}`,
);
}
}
if (newOptions.appCopyright === undefined) {
// https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L54-L56
newOptions.appCopyright = getVersionString(
executablePath,
'LegalCopyright',
);
log.debug(
`Extracted app copyright from executable: ${
newOptions.appCopyright as string
}`,
);
}
} else if (looksLikeLinux) {
log.debug('This looks like a Linux app...');
if (newOptions.platform === undefined) {
newOptions.platform = 'linux';
}
executablePath = path.join(
appRoot,
children.filter((c) => c.name == name && c.isFile())[0].name,
);
}
if (!executablePath || !newOptions.platform) {
throw Error(
`Could not find executablePath or platform of app in ${appRoot}`,
);
}
log.debug(`Executable path: ${executablePath}`);
if (newOptions.arch === undefined) {
const executableInfo = getExecutableInfo(
executablePath,
newOptions.platform,
);
if (!executableInfo) {
throw new Error(
`Could not get executable info for executable path: ${executablePath}`,
);
}
newOptions.arch = executableInfo.arch;
log.debug(`Extracted arch from executable: ${newOptions.arch as string}`);
}
if (newOptions.platform === undefined || newOptions.arch == undefined) {
throw Error(`Could not determine platform / arch of app in ${appRoot}`);
}
return newOptions;
}

View File

@ -0,0 +1,39 @@
export function extractBoolean(
infoPlistXML: string,
plistKey: string,
): boolean | undefined {
const plistValue = extractRaw(infoPlistXML, plistKey);
return plistValue === undefined
? undefined
: plistValue.split('<')[1].split('/>')[0].toLowerCase() === 'true';
}
export function extractString(
infoPlistXML: string,
plistKey: string,
): string | undefined {
const plistValue = extractRaw(infoPlistXML, plistKey);
return plistValue === undefined
? undefined
: plistValue.split('<string>')[1].split('</string>')[0];
}
function extractRaw(
infoPlistXML: string,
plistKey: string,
): string | undefined {
// This would be easier with xml2js, but let's not add a dependency for something this minor.
const fullKey = `\n <key>${plistKey}</key>`;
if (infoPlistXML.indexOf(fullKey) === -1) {
// This value wasn't set, so we'll stay agnostic to it
return undefined;
}
return infoPlistXML
.split(fullKey)[1]
.split('\n </dict>')[0] // Get everything between here and the end of the main plist dict
.split('\n <key>')[0]; // Get everything before the next key (if it exists)
}

View File

@ -0,0 +1,42 @@
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'child_process';
// A modification of https://github.com/electron/node-rcedit to support the retrieval
// of information.
export function getVersionString(
executablePath: string,
versionString: string,
): string | undefined {
let rcedit = path.resolve(
__dirname,
'..',
'..',
'..',
'node_modules',
'rcedit',
'bin',
process.arch === 'x64' ? 'rcedit-x64.exe' : 'rcedit.exe',
);
const args = [executablePath, `--get-version-string`, versionString];
const spawnOptions = {
env: { ...process.env },
};
// Use Wine on non-Windows platforms except for WSL, which doesn't need it
if (process.platform !== 'win32' && !os.release().endsWith('Microsoft')) {
args.unshift(rcedit);
rcedit = process.arch === 'x64' ? 'wine64' : 'wine';
// Suppress "fixme:" stderr log messages
spawnOptions.env.WINEDEBUG = '-all';
}
try {
const child = spawnSync(rcedit, args, spawnOptions);
const result = child.output?.toString().split(',wine: ')[0];
return result.startsWith(',') ? result.substr(1) : result;
} catch {
return undefined;
}
}

View File

@ -0,0 +1,234 @@
import * as fs from 'fs';
import * as path from 'path';
import * as log from 'loglevel';
import {
NativefierOptions,
RawOptions,
} from '../../../shared/src/options/model';
import { dirExists, fileExists } from '../fsHelpers';
import { extractBoolean, extractString } from './plistInfoXMLHelpers';
import { getOptionsFromExecutable } from './executableHelpers';
import { parseJson } from '../../utils/parseUtils';
export type UpgradeAppInfo = {
appResourcesDir: string;
appRoot: string;
options: NativefierOptions;
};
function findUpgradeAppResourcesDir(searchDir: string): string | null {
searchDir = dirExists(searchDir) ? searchDir : path.dirname(searchDir);
log.debug(`Searching for nativfier.json in ${searchDir}`);
const children = fs.readdirSync(searchDir, { withFileTypes: true });
if (fileExists(path.join(searchDir, 'nativefier.json'))) {
// Found 'nativefier.json', so this must be it!
return path.resolve(searchDir);
}
const childDirectories = children.filter((c) => c.isDirectory());
for (const childDir of childDirectories) {
// We must go deeper!
const result = findUpgradeAppResourcesDir(
path.join(searchDir, childDir.name, 'nativefier.json'),
);
if (result !== null) {
return path.resolve(result);
}
}
// Didn't find it down here
return null;
}
function getAppRoot(
appResourcesDir: string,
options: NativefierOptions,
): string {
switch (options.platform) {
case 'darwin':
return path.resolve(path.join(appResourcesDir, '..', '..', '..', '..'));
case 'linux':
case 'win32':
return path.resolve(path.join(appResourcesDir, '..', '..'));
default:
throw new Error(
`Could not find the app root for platform: ${
options.platform ?? 'undefined'
}`,
);
}
}
function getIconPath(appResourcesDir: string): string | undefined {
const icnsPath = path.join(appResourcesDir, '..', 'electron.icns');
if (fileExists(icnsPath)) {
log.debug(`Found icon at: ${icnsPath}`);
return path.resolve(icnsPath);
}
const icoPath = path.join(appResourcesDir, 'icon.ico');
if (fileExists(icoPath)) {
log.debug(`Found icon at: ${icoPath}`);
return path.resolve(icoPath);
}
const pngPath = path.join(appResourcesDir, 'icon.png');
if (fileExists(pngPath)) {
log.debug(`Found icon at: ${pngPath}`);
return path.resolve(pngPath);
}
log.debug('Could not find icon file.');
return undefined;
}
function getInfoPListOptions(
appResourcesDir: string,
priorOptions: NativefierOptions,
): NativefierOptions {
if (!fileExists(path.join(appResourcesDir, '..', '..', 'Info.plist'))) {
// Not a darwin/mas app, so this is irrelevant
return priorOptions;
}
const newOptions = { ...priorOptions };
const infoPlistXML: string = fs
.readFileSync(path.join(appResourcesDir, '..', '..', 'Info.plist'))
.toString();
if (newOptions.appCopyright === undefined) {
// https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L230-L232
newOptions.appCopyright = extractString(
infoPlistXML,
'NSHumanReadableCopyright',
);
log.debug(
`Extracted app copyright from Info.plist: ${
newOptions.appCopyright as string
}`,
);
}
if (newOptions.appVersion === undefined) {
// https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L214-L216
// This could also be the buildVersion, but since they end up in the same place, that SHOULDN'T matter
const bundleVersion = extractString(infoPlistXML, 'CFBundleVersion');
newOptions.appVersion =
bundleVersion === undefined || bundleVersion === '1.0.0' // If it's 1.0.0, that's just the default
? undefined
: bundleVersion;
(newOptions.darwinDarkModeSupport =
newOptions.darwinDarkModeSupport === undefined
? undefined
: newOptions.darwinDarkModeSupport === false),
log.debug(
`Extracted app version from Info.plist: ${
newOptions.appVersion as string
}`,
);
}
if (newOptions.darwinDarkModeSupport === undefined) {
// https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L234-L236
newOptions.darwinDarkModeSupport = extractBoolean(
infoPlistXML,
'NSRequiresAquaSystemAppearance',
);
log.debug(
`Extracted Darwin dark mode support from Info.plist: ${
newOptions.darwinDarkModeSupport ? 'Yes' : 'No'
}`,
);
}
return newOptions;
}
function getInjectPaths(appResourcesDir: string): string[] | undefined {
const injectDir = path.join(appResourcesDir, 'inject');
if (!dirExists(injectDir)) {
return undefined;
}
const injectPaths = fs
.readdirSync(injectDir, { withFileTypes: true })
.filter(
(fd) =>
fd.isFile() &&
(fd.name.toLowerCase().endsWith('.css') ||
fd.name.toLowerCase().endsWith('.js')),
)
.map((fd) => path.resolve(path.join(injectDir, fd.name)));
log.debug(`CSS/JS Inject paths: ${injectPaths.join(', ')}`);
return injectPaths;
}
function isAsar(appResourcesDir: string): boolean {
const asar = fileExists(path.join(appResourcesDir, '..', 'electron.asar'));
log.debug(`Is this app an ASAR? ${asar ? 'Yes' : 'No'}`);
return asar;
}
export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null {
const searchDir = dirExists(upgradeFrom)
? upgradeFrom
: path.dirname(upgradeFrom);
log.debug(`Looking for old options file in ${searchDir}`);
const appResourcesDir = findUpgradeAppResourcesDir(searchDir);
if (appResourcesDir === null) {
log.debug(`No nativefier.json file found in ${searchDir}`);
return null;
}
const nativefierJSONPath = path.join(appResourcesDir, 'nativefier.json');
log.debug(`Loading ${nativefierJSONPath}`);
let options = parseJson<NativefierOptions>(
fs.readFileSync(nativefierJSONPath, 'utf8'),
);
if (!options) {
throw new Error(
`Could not read Nativefier options from ${nativefierJSONPath}`,
);
}
options.electronVersion = undefined;
options = {
...options,
...getOptionsFromExecutable(appResourcesDir, options),
};
const appRoot = getAppRoot(appResourcesDir, options);
return {
appResourcesDir,
appRoot,
options: {
...options,
...getInfoPListOptions(appResourcesDir, options),
asar: options.asar !== undefined ? options.asar : isAsar(appResourcesDir),
icon: getIconPath(appResourcesDir),
inject: getInjectPaths(appResourcesDir),
},
};
}
export function useOldAppOptions(
rawOptions: RawOptions,
oldApp: UpgradeAppInfo,
): RawOptions {
if (rawOptions.targetUrl !== undefined && dirExists(rawOptions.targetUrl)) {
// You got your ouput dir in my targetUrl!
rawOptions.out = rawOptions.targetUrl;
}
log.debug('oldApp', oldApp);
const combinedOptions = { ...rawOptions, ...oldApp.options };
log.debug('Combined options', combinedOptions);
return combinedOptions;
}

View File

@ -0,0 +1,58 @@
import axios from 'axios';
import * as log from 'loglevel';
import {
DEFAULT_CHROME_VERSION,
DEFAULT_ELECTRON_VERSION,
} from '../../constants';
type ElectronRelease = {
version: string;
date: string;
node: string;
v8: string;
uv: string;
zlib: string;
openssl: string;
modules: string;
chrome: string;
files: string[];
};
const ELECTRON_VERSIONS_URL = 'https://releases.electronjs.org/releases.json';
export async function getChromeVersionForElectronVersion(
electronVersion: string,
url = ELECTRON_VERSIONS_URL,
): Promise<string> {
if (!electronVersion || electronVersion === DEFAULT_ELECTRON_VERSION) {
// Exit quickly for the scenario that we already know about
return DEFAULT_CHROME_VERSION;
}
try {
log.debug('Grabbing electron<->chrome versions file from', url);
const response = await axios.get<ElectronRelease[]>(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
const electronReleases: ElectronRelease[] = response.data;
const electronVersionToChromeVersion: { [key: string]: string } = {};
for (const release of electronReleases) {
electronVersionToChromeVersion[release.version] = release.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;
} catch (err: unknown) {
log.error('getChromeVersionForElectronVersion ERROR', err);
log.debug('Falling back to default Chrome version', DEFAULT_CHROME_VERSION);
return DEFAULT_CHROME_VERSION;
}
}

View File

@ -0,0 +1,49 @@
import axios from 'axios';
import * as log from 'loglevel';
import { DEFAULT_FIREFOX_VERSION } from '../../constants';
type FirefoxVersions = {
FIREFOX_AURORA: string;
FIREFOX_DEVEDITION: string;
FIREFOX_ESR: string;
FIREFOX_ESR_NEXT: string;
FIREFOX_NIGHTLY: string;
LAST_MERGE_DATE: string;
LAST_RELEASE_DATE: string;
LAST_SOFTFREEZE_DATE: string;
LATEST_FIREFOX_DEVEL_VERSION: string;
LATEST_FIREFOX_OLDER_VERSION: string;
LATEST_FIREFOX_RELEASED_DEVEL_VERSION: string;
LATEST_FIREFOX_VERSION: string;
NEXT_MERGE_DATE: string;
NEXT_RELEASE_DATE: string;
NEXT_SOFTFREEZE_DATE: string;
};
const FIREFOX_VERSIONS_URL =
'https://product-details.mozilla.org/1.0/firefox_versions.json';
export async function getLatestFirefoxVersion(
url = FIREFOX_VERSIONS_URL,
): Promise<string> {
try {
log.debug('Grabbing Firefox version data from', url);
const response = await axios.get<FirefoxVersions>(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
const firefoxVersions: FirefoxVersions = response.data;
log.debug(
`Got latest Firefox version ${firefoxVersions.LATEST_FIREFOX_VERSION}`,
);
return firefoxVersions.LATEST_FIREFOX_VERSION;
} catch (err: unknown) {
log.error('getLatestFirefoxVersion ERROR', err);
log.debug(
'Falling back to default Firefox version',
DEFAULT_FIREFOX_VERSION,
);
return DEFAULT_FIREFOX_VERSION;
}
}

View File

@ -0,0 +1,77 @@
import axios from 'axios';
import * as log from 'loglevel';
import { DEFAULT_SAFARI_VERSION } from '../../constants';
export type SafariVersion = {
majorVersion: number;
version: string;
webkitVersion: string;
};
const SAFARI_VERSIONS_HISTORY_URL =
'https://en.wikipedia.org/wiki/Safari_version_history';
export async function getLatestSafariVersion(
url = SAFARI_VERSIONS_HISTORY_URL,
): Promise<SafariVersion> {
try {
log.debug('Grabbing apple version data from', url);
const response = await axios.get<string>(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
// This would be easier with an HTML parser, but we're not going to include an extra dependency for something that dumb
const rawData: string = response.data;
const majorVersions = [
...rawData.matchAll(
/class="mw-headline" id="Safari_[0-9]*">Safari ([0-9]*)</g,
),
].map((match) => match[1]);
const majorVersion = parseInt(majorVersions[majorVersions.length - 1]);
const majorVersionTable = rawData
.split('>Release history<')[2]
.split('<table')
.filter((table) => table.includes(`Safari ${majorVersion}.x`))[0];
const versionRows = majorVersionTable.split('<tbody')[1].split('<tr');
let version: string | undefined = undefined;
let webkitVersion: string | undefined = undefined;
for (const versionRow of versionRows.reverse()) {
const versionMatch = [
...versionRow.matchAll(/>\s*(([0-9]*\.){2}[0-9])\s*</g),
];
if (versionMatch.length > 0 && !version) {
version = versionMatch[0][1];
}
const webkitVersionMatch = [
...versionRow.matchAll(/>\s*(([0-9]*\.){3,4}[0-9])\s*</g),
];
if (webkitVersionMatch.length > 0 && !webkitVersion) {
webkitVersion = webkitVersionMatch[0][1];
}
if (version && webkitVersion) {
break;
}
}
if (version && webkitVersion) {
return {
majorVersion,
version,
webkitVersion,
};
}
return DEFAULT_SAFARI_VERSION;
} catch (err: unknown) {
log.error('getLatestSafariVersion ERROR', err);
log.debug('Falling back to default Safari version', DEFAULT_SAFARI_VERSION);
return DEFAULT_SAFARI_VERSION;
}
}

View File

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

View File

@ -1,15 +1,23 @@
import * as os from 'os'; import * as os from 'os';
import * as log from 'loglevel'; import * as log from 'loglevel';
// Ideally we'd get this list directly from electron-packager, but it's not
// possible to convert a literal type to an array of strings in current TypeScript
export const supportedArchs = ['x64', 'armv7l', 'arm64', 'universal'];
export const supportedPlatforms = [
'darwin',
'linux',
'mac',
'mas',
'osx',
'win32',
'windows',
];
export function inferPlatform(): string { export function inferPlatform(): string {
const platform = os.platform(); const platform = os.platform();
if ( if (['darwin', 'linux', 'win32'].includes(platform)) {
platform === 'darwin' ||
// @ts-ignore
platform === 'mas' ||
platform === 'win32' ||
platform === 'linux'
) {
log.debug('Inferred platform', platform); log.debug('Inferred platform', platform);
return platform; return platform;
} }
@ -19,7 +27,7 @@ export function inferPlatform(): string {
export function inferArch(): string { export function inferArch(): string {
const arch = os.arch(); const arch = os.arch();
if (arch !== 'ia32' && arch !== 'x64' && arch !== 'arm' && arch !== 'arm64') { if (!supportedArchs.includes(arch)) {
throw new Error(`Incompatible architecture ${arch} detected`); throw new Error(`Incompatible architecture ${arch} detected`);
} }
log.debug('Inferred arch', arch); log.debug('Inferred arch', arch);

View File

@ -1,4 +1,4 @@
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { inferTitle } from './inferTitle'; import { inferTitle } from './inferTitle';
@ -14,7 +14,8 @@ test('it returns the correct title', async () => {
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
headers: {}, headers: {},
config: {}, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
config: {} as unknown as InternalAxiosRequestConfig<unknown>,
}; };
axiosGetMock.mockResolvedValue(mockedResponse); axiosGetMock.mockResolvedValue(mockedResponse);
const result = await inferTitle('someurl'); const result = await inferTitle('someurl');

View File

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

View File

@ -1,29 +0,0 @@
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',
);
});
});

View File

@ -1,82 +0,0 @@
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);
}
}

View File

@ -1,57 +1,220 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { DEFAULT_ELECTRON_VERSION } from './constants';
import { getTempDir } from './helpers/helpers'; import { getTempDir } from './helpers/helpers';
import { getChromeVersionForElectronVersion } from './infer/browsers/inferChromeVersion';
import { getLatestFirefoxVersion } from './infer/browsers/inferFirefoxVersion';
import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion';
import { inferArch } from './infer/inferOs';
import { buildNativefierApp } from './main'; import { buildNativefierApp } from './main';
import { userAgent } from './options/fields/userAgent';
import {
GlobalShortcut,
NativefierOptions,
RawOptions,
} from '../shared/src/options/model';
import { parseJson } from './utils/parseUtils';
function checkApp(appRoot: string, inputOptions: any): void { async function checkApp(
let relativeAppFolder: string; appRoot: string,
inputOptions: RawOptions,
switch (inputOptions.platform) { ): Promise<void> {
case 'darwin': const arch = inputOptions.arch ? inputOptions.arch : inferArch();
relativeAppFolder = path.join('Google.app', 'Contents/Resources/app'); if (inputOptions.out !== undefined) {
break; expect(
case 'linux': path.join(
relativeAppFolder = 'resources/app'; inputOptions.out,
break; `npm-${inputOptions.platform as string}-${arch}`,
case 'win32': ),
relativeAppFolder = 'resources/app'; ).toBe(appRoot);
break;
default:
throw new Error('Unknown app platform');
} }
const appPath = path.join(appRoot, relativeAppFolder); let relativeResourcesDir = 'resources';
if (inputOptions.platform === 'darwin') {
relativeResourcesDir = path.join('npm.app', 'Contents', 'Resources');
}
const appPath = path.join(appRoot, relativeResourcesDir, 'app');
const configPath = path.join(appPath, 'nativefier.json'); const configPath = path.join(appPath, 'nativefier.json');
const nativefierConfig = JSON.parse(fs.readFileSync(configPath).toString()); const nativefierConfig: NativefierOptions | undefined =
expect(inputOptions.targetUrl).toBe(nativefierConfig.targetUrl); parseJson<NativefierOptions>(fs.readFileSync(configPath).toString());
expect(nativefierConfig).not.toBeUndefined();
expect(inputOptions.targetUrl).toBe(nativefierConfig?.targetUrl);
// Test name inferring // Test name inferring
expect(nativefierConfig.name).toBe('Google'); expect(nativefierConfig?.name).toBe('npm');
// Test icon writing // Test icon writing
const iconFile = const iconFile =
inputOptions.platform === 'darwin' ? '../electron.icns' : 'icon.png'; inputOptions.platform === 'darwin'
? path.join('..', 'electron.icns')
: inputOptions.platform === 'linux'
? 'icon.png'
: 'icon.ico';
const iconPath = path.join(appPath, iconFile); const iconPath = path.join(appPath, iconFile);
expect(fs.existsSync(iconPath)).toBe(true); expect(fs.existsSync(iconPath)).toEqual(true);
expect(fs.statSync(iconPath).size).toBeGreaterThan(1000); expect(fs.statSync(iconPath).size).toBeGreaterThan(1000);
// Test arch
if (inputOptions.arch !== undefined) {
expect(inputOptions.arch).toEqual(nativefierConfig?.arch);
} else {
expect(os.arch()).toEqual(nativefierConfig?.arch);
}
// Test electron version
expect(nativefierConfig?.electronVersionUsed).toBe(
inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION,
);
// Test user agent
if (inputOptions.userAgent) {
const translatedUserAgent = await userAgent({
packager: {
platform: inputOptions.platform,
electronVersion:
inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION,
},
nativefier: { userAgent: inputOptions.userAgent },
});
inputOptions.userAgent = translatedUserAgent || inputOptions.userAgent;
}
expect(nativefierConfig?.userAgent).toEqual(inputOptions.userAgent);
// Test lang
expect(nativefierConfig?.lang).toEqual(inputOptions.lang);
// Test global shortcuts
if (inputOptions.globalShortcuts) {
let shortcutData: GlobalShortcut[] | undefined = [];
if (typeof inputOptions.globalShortcuts === 'string') {
shortcutData = parseJson<GlobalShortcut[]>(
fs.readFileSync(inputOptions.globalShortcuts, 'utf8'),
);
} else {
shortcutData = inputOptions.globalShortcuts;
}
expect(nativefierConfig?.globalShortcuts).toStrictEqual(shortcutData);
}
} }
describe('Nativefier', () => { describe('Nativefier', () => {
jest.setTimeout(300000); jest.setTimeout(300000);
test('builds a Nativefier app for several platforms', async () => { test.each(['darwin', 'linux'])(
for (const platform of ['darwin', 'linux']) { 'builds a Nativefier app for platform %s',
async (platform) => {
const tempDirectory = getTempDir('integtest'); const tempDirectory = getTempDir('integtest');
const options = { const options: RawOptions = {
targetUrl: 'https://google.com/', lang: 'en-US',
out: tempDirectory, out: tempDirectory,
overwrite: true, overwrite: true,
platform, platform,
targetUrl: 'https://npmjs.com/',
}; };
const appPath = await buildNativefierApp(options); const appPath = await buildNativefierApp(options);
checkApp(appPath, options); expect(appPath).not.toBeUndefined();
await checkApp(appPath, options);
},
);
});
function generateShortcutsFile(dir: string): string {
const shortcuts = [
{
key: 'MediaPlayPause',
inputEvents: [
{
type: 'keyDown',
keyCode: 'Space',
},
],
},
{
key: 'MediaNextTrack',
inputEvents: [
{
type: 'keyDown',
keyCode: 'Right',
},
],
},
];
const filename = path.join(dir, 'shortcuts.json');
fs.writeFileSync(filename, JSON.stringify(shortcuts));
return filename;
} }
describe('Nativefier upgrade', () => {
jest.setTimeout(300000);
test.each([
{ platform: 'darwin', arch: 'x64' },
{ platform: 'linux', arch: 'arm64', userAgent: 'FIREFOX 60' },
// Exhaustive integration testing here would be neat, but takes too long.
// -> For now, only testing a subset of platforms/archs
// { platform: 'win32', arch: 'x64' },
// { platform: 'darwin', arch: 'arm64' },
// { platform: 'linux', arch: 'x64' },
// { platform: 'linux', arch: 'armv7l' },
])(
'can upgrade a Nativefier app for platform/arch: %s',
async (baseAppOptions) => {
const tempDirectory = getTempDir('integtestUpgrade1');
const shortcuts = generateShortcutsFile(tempDirectory);
const options: RawOptions = {
electronVersion: '11.2.3',
globalShortcuts: shortcuts,
out: tempDirectory,
overwrite: true,
targetUrl: 'https://npmjs.com/',
...baseAppOptions,
};
const appPath = await buildNativefierApp(options);
expect(appPath).not.toBeUndefined();
await checkApp(appPath, options);
const upgradeOptions: RawOptions = {
upgrade: appPath,
overwrite: true,
};
const upgradeAppPath = await buildNativefierApp(upgradeOptions);
options.electronVersion = DEFAULT_ELECTRON_VERSION;
options.userAgent = baseAppOptions.userAgent;
expect(upgradeAppPath).not.toBeUndefined();
await checkApp(upgradeAppPath, options);
},
);
});
describe('Browser version retrieval', () => {
test('get chrome version with electron version', async () => {
await expect(getChromeVersionForElectronVersion('12.0.0')).resolves.toBe(
'89.0.4389.69',
);
});
test('get latest firefox version', async () => {
const firefoxVersion = await getLatestFirefoxVersion();
const majorVersion = parseInt(firefoxVersion.split('.')[0]);
expect(majorVersion).toBeGreaterThanOrEqual(88);
});
test('get latest safari version', async () => {
const safariVersion = await getLatestSafariVersion();
expect(safariVersion.majorVersion).toBeGreaterThanOrEqual(14);
}); });
}); });

View File

@ -5,3 +5,5 @@ if (process.env.LOGLEVEL) {
} else { } else {
log.disableAll(); log.disableAll();
} }
process.traceDeprecation = true;

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More