├── .babelignore ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .npmrc ├── .tern-project ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── package-lock.json ├── package.json ├── src ├── conversion.js ├── converters │ └── pdf.js ├── dedicatedProcessStrategy.js ├── ensureStartWorker.js ├── index.js ├── ipc.js ├── pdfParser │ └── index.js ├── saveFile.js ├── scripts │ ├── conversionScript.js │ ├── evaluateJS.js │ ├── evaluateTemplate.js │ ├── getBrowserWindowOpts.js │ ├── ipcScript.js │ ├── listenRequestsInPage.js │ ├── preload.js │ ├── serverScript.js │ └── standaloneScript.js └── serverIpcStrategy.js └── test ├── .eslintrc ├── mocha.opts └── test.js /.babelignore: -------------------------------------------------------------------------------- 1 | src/pdfParser/pdf.combined.js 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # Recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.py] 20 | indent_style = space 21 | indent_size = 4 22 | 23 | [*.js] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | [*.css] 28 | indent_style = space 29 | indent_size = 2 30 | 31 | [*.jade] 32 | indent_style = space 33 | indent_size = 2 34 | 35 | [*.md] 36 | trim_trailing_whitespace = false 37 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | **/node_modules 3 | src/pdfParser/pdf.combined.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb-base", 3 | 4 | // Allow the following global variables 5 | "env": { 6 | "node": true // Node.js global variables and Node.js scoping. 7 | }, 8 | 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | experimentalObjectRestSpread: true 12 | } 13 | }, 14 | 15 | "rules": { 16 | "strict": [2, "safe"], 17 | 18 | /** 19 | * ES6 20 | */ 21 | "prefer-const": 0, 22 | 23 | /** 24 | * Variables 25 | */ 26 | "no-shadow": [2, { 27 | "builtinGlobals": true 28 | }], 29 | "no-unused-vars": [2, { 30 | "vars": "all", 31 | "args": "after-used" 32 | }], 33 | "no-use-before-define": [2, "nofunc"], 34 | 35 | /** 36 | * Possible errors 37 | */ 38 | "comma-dangle": [2, "never"], 39 | "no-inner-declarations": [2, "both"], 40 | 41 | /** 42 | * Best practices 43 | */ 44 | "consistent-return": 0, 45 | "curly": 2, 46 | "dot-notation": [2, { 47 | "allowKeywords": true, 48 | "allowPattern": "^[a-z]+(_[a-z]+)+$" 49 | }], 50 | "eqeqeq": [2, "allow-null"], 51 | "no-eq-null": 0, 52 | "no-redeclare": [2, { 53 | "builtinGlobals": true 54 | }], 55 | "wrap-iife": [2, "inside"], 56 | "max-len": [2, 130, 2, {"ignoreUrls": true}], 57 | 58 | /** 59 | * Style 60 | */ 61 | "indent": [2, 2, { 62 | "VariableDeclarator": { 63 | "var": 2, 64 | "let": 2, 65 | "const": 3 66 | }, 67 | "SwitchCase": 1 68 | }], 69 | "func-names": 0, 70 | "no-multiple-empty-lines": [2, { 71 | "max": 1 72 | }], 73 | "no-extra-parens": [2, "functions"], 74 | "one-var": 0, 75 | "space-before-function-paren": [2, "never"], 76 | "no-underscore-dangle": 0 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Git to autodetect text files and normalise their line endings to LF when they are checked into your repository. 2 | * text=auto 3 | 4 | # 5 | # The above will handle all files NOT found below 6 | # 7 | 8 | # These files are text and should be normalized (Convert crlf => lf) 9 | *.php text 10 | *.css text 11 | *.js text 12 | *.htm text 13 | *.html text 14 | *.xml text 15 | *.txt text 16 | *.ini text 17 | *.inc text 18 | .htaccess text 19 | 20 | # These files are binary and should be left untouched 21 | # (binary is a macro for -text -diff) 22 | *.png binary 23 | *.jpg binary 24 | *.jpeg binary 25 | *.gif binary 26 | *.ico binary 27 | *.mov binary 28 | *.mp4 binary 29 | *.mp3 binary 30 | *.flv binary 31 | *.fla binary 32 | *.swf binary 33 | *.gz binary 34 | *.zip binary 35 | *.7z binary 36 | *.ttf binary 37 | 38 | # Documents 39 | *.doc diff=astextplain 40 | *.DOC diff=astextplain 41 | *.docx diff=astextplain 42 | *.DOCX diff=astextplain 43 | *.dot diff=astextplain 44 | *.DOT diff=astextplain 45 | *.pdf diff=astextplain 46 | *.PDF diff=astextplain 47 | *.rtf diff=astextplain 48 | *.RTF diff=astextplain 49 | 50 | # These files are text and should be normalized (Convert crlf => lf) 51 | *.gitattributes text 52 | .gitignore text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | log 3 | *.log 4 | npm-debug.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | # Deployed apps should consider commenting this line out: 25 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 26 | node_modules 27 | bower_components 28 | 29 | # Temp directory 30 | .DS_Store 31 | .tmp 32 | .sass-cache 33 | test/temp 34 | 35 | # compiled es5 code 36 | lib 37 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | src 4 | test 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 6, 3 | "libs": [], 4 | "plugins": { 5 | "node": {}, 6 | "modules": {}, 7 | "es_modules": {} 8 | } 9 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.9" 4 | - "4.1" 5 | - "4.0" 6 | - "0.10" 7 | install: 8 | - export DISPLAY=':99.0' 9 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 10 | - npm install 11 | addons: 12 | apt: 13 | packages: 14 | - xvfb 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 BJR Matos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | electron-html-to 2 | ================ 3 | 4 | [![NPM Version](http://img.shields.io/npm/v/electron-html-to.svg?style=flat-square)](https://npmjs.com/package/electron-html-to)[![License](http://img.shields.io/npm/l/electron-html-to.svg?style=flat-square)](http://opensource.org/licenses/MIT)[![Build Status](https://travis-ci.org/bjrmatos/electron-html-to.png?branch=master)](https://travis-ci.org/bjrmatos/electron-html-to) 5 | 6 | > **Highly scalable html conversion in scale** 7 | 8 | This module let you convert a web page (html, css, js) in any format you want (via a converter function) using [electron](http://electron.atom.io/). 9 | 10 | *Works in electron@>=0.36.1 including electron@1* 11 | 12 | ```js 13 | var fs = require('fs'), 14 | convertFactory = require('electron-html-to'); 15 | 16 | var conversion = convertFactory({ 17 | converterPath: convertFactory.converters.PDF 18 | }); 19 | 20 | conversion({ html: '

Hello World

' }, function(err, result) { 21 | if (err) { 22 | return console.error(err); 23 | } 24 | 25 | console.log(result.numberOfPages); 26 | console.log(result.logs); 27 | result.stream.pipe(fs.createWriteStream('/path/to/anywhere.pdf')); 28 | conversion.kill(); // necessary if you use the electron-server strategy, see bellow for details 29 | }); 30 | ``` 31 | 32 | Built-in converters 33 | ------------------- 34 | 35 | - `convertFactory.converters.PDF` (html to pdf) -> when the conversion ends the `result` param will have `numberOfPages` (Number) and `stream` (Stream) properties. 36 | 37 | Custom converters 38 | ----------------- 39 | 40 | Converters are functions that run in the electron process, see the [pdf conversion implementation](https://github.com/bjrmatos/electron-html-to/blob/master/src/converters/pdf.js) for an example. 41 | 42 | Global options 43 | -------------- 44 | 45 | ```js 46 | var conversion = require('electron-html-to')({ 47 | /* optional absolute path to a custom electron executable, if not passed we will try to detect the path of the electron executable installed */ 48 | pathToElectron: '/path/to/custom/electron-executable', 49 | /* optional array of custom arguments to pass to the electron executable */ 50 | electronArgs: ['--some-value=2', '--enable-some-behaviour'], 51 | /* required absolute path to the converter function to use, every conversion will use the converter specified */ 52 | converterPath: '/path/to/a/converter.js' 53 | /* number of allocated electron processes (when using electron-server strategy). defaults to 2 */ 54 | numberOfWorkers: 2, 55 | /* time in ms to wait for worker ping response in order to be considered alive when using `electron-server` or `electron-ipc` strategy, see https://github.com/bjrmatos/electron-workers#options for details */ 56 | pingTimeout: 100, 57 | /* timeout in ms for html conversion, when the timeout is reached, the conversion is cancelled. defaults to 180000ms */ 58 | timeout: 5000, 59 | /* directory where are stored temporary html and pdf files, use something like npm package reap to clean this up */ 60 | tmpDir: 'os/tmpdir', 61 | /* optional port range where to start electron server (when using electron-server strategy) */ 62 | portLeftBoundary: 1000, 63 | portRightBoundary: 2000, 64 | /* optional hostname where to start electron server when using electron-server strategy) */ 65 | host: '127.0.0.1', 66 | /* set to true to allow request using the file protocol (file:///). defaults to false */ 67 | allowLocalFilesAccess: false, 68 | /* the collected console.log, console.error, console.warn messages are trimmed by default */ 69 | maxLogEntrySize: 1000, 70 | /* optional chrome command line switches, see http://electron.atom.io/docs/v0.36.1/api/chrome-command-line-switches/ for details. defaults to { 'ignore-certificate-errors': null } */ 71 | chromeCommandLineSwitches: { 72 | 'disable-http-cache': null, 73 | 'log-net-log': '/path/to/save' 74 | }, 75 | /* use rather dedicated process for every conversion, 76 | dedicated-process strategy is quite slower but can solve some bugs 77 | with corporate proxy. for a description of `electron-server` and `electron-ipc` strategy see [electron-workers docs](https://github.com/bjrmatos/electron-workers/#modes). defaults to electron-ipc strategy */ 78 | strategy: 'electron-ipc | electron-server | dedicated-process' 79 | }); 80 | ``` 81 | 82 | Local options 83 | ------------- 84 | 85 | ```js 86 | conversion({ 87 | html: '

Hello world

', 88 | url: 'http://jsreport.net', // set direct url instead of html 89 | delay: 0, // time in ms to wait before the conversion 90 | // boolean that specifies if we should collect logs calls (console.log, console.error, console.warn) in webpage 91 | // logs will be available as result.logs after the conversion 92 | // defaults to true 93 | collectLogs: true, 94 | waitForJS: true, // set to true to enable programmatically specify (via Javascript of the page) when the conversion starts (see Programmatic conversion section for an example) 95 | waitForJSVarName: 'MY_CUSTOM_VAR_NAME', // name of the variable that will be used as the conversion trigger, defaults to "ELECTRON_HTML_TO_READY" (see Programmatic pdf printing section for an example) 96 | userAgent: 'CUSTOM_USER_AGENT', // set a custom user agent to use in electron's browser window 97 | /* custom extra headers to load the html or url */ 98 | extraHeaders: { 99 | 'X-Foo': 'foo', 100 | 'X-Bar': 'bar' 101 | }, 102 | converterPath: '/path/to/a/converter.js', // absolute path to the converter function to use in the local conversion, if no specified the global converterPath option will be used 103 | 104 | // options for electron's browser window, see http://electron.atom.io/docs/v0.36.1/api/browser-window/ for details for each option. 105 | // allowed browser-window options 106 | browserWindow: { 107 | width: 600, // defaults to 600 108 | height: 600, // defaults to 600 109 | x: 0, 110 | y: 0, 111 | useContentSize: false, 112 | webPreferences: { 113 | nodeIntegration: false, // defaults to false 114 | partition: '', 115 | zoomFactor: 3.0, 116 | javascript: true, // defaults to true 117 | webSecurity: false, // defaults to false 118 | allowDisplayingInsecureContent: true, 119 | allowRunningInsecureContent: true, 120 | images: true, 121 | java: true, 122 | webgl: true, 123 | webaudio: true, 124 | plugins: , 125 | experimentalFeatures: , 126 | experimentalCanvasFeatures: , 127 | overlayScrollbars: , 128 | overlayFullscreenVideo: , 129 | sharedWorker: , 130 | directWrite: 131 | } 132 | }, 133 | 134 | // options to the pdf converter function, see electron's printoToPDF function http://electron.atom.io/docs/v0.36.1/api/web-contents/#webcontents-printtopdf-options-callback for details for each option. 135 | // allowed printToPDF options 136 | pdf: { 137 | marginsType: 0, 138 | pageSize: 'A4', 139 | printBackground: false, 140 | landscape: false 141 | } 142 | }, cb); 143 | ``` 144 | 145 | Local resources 146 | --------------- 147 | 148 | You can add local files like `.css`, `.jpg` or `.js` files by setting the 149 | `allowLocalFilesAccess` option to _true_. This option allow requests with the file protocol `file:///`. 150 | 151 | ### Example: 152 | 153 | ```html 154 | 155 | 156 | 157 | 158 | 159 |

It Works!!

160 | 161 | 162 | ``` 163 | 164 | If your html doesn't have url in the form of `file://path/to/you/base/public/directory` you would need to transform paths from `/images/company_logo.jpg` to `file://path/to/you/base/public/directory/images/company_logo.jpg`. 165 | 166 | 167 | ```js 168 | const fs = require('fs'); 169 | const convertFactory = require('electron-html-to'); 170 | fs.readFile('index.html', 'utf8', (err, htmlString) => { 171 | // add local path in case your HTML has relative paths 172 | htmlString = htmlString.replace(/href="|src="/g, match => { 173 | return match + 'file://path/to/you/base/public/directory'; 174 | }); 175 | const conversion = convertFactory({ 176 | converterPath: convertFactory.converters.PDF, 177 | allowLocalFilesAccess: true 178 | }); 179 | conversion({ html: htmlString }, (err, result) => { 180 | if (err) return console.error(err); 181 | result.stream.pipe(fs.createWriteStream('/path/to/anywhere.pdf')); 182 | conversion.kill(); // necessary if you use the electron-server strategy, see bellow for details 183 | }); 184 | }); 185 | ``` 186 | 187 | Kill workers 188 | ------------ 189 | 190 | ```js 191 | // kill all electron workers when using electron-server strategy 192 | conversion.kill(); 193 | ``` 194 | 195 | Programmatic conversion 196 | ----------------------- 197 | 198 | If you need to programmatic trigger the conversion process (because you need to calculate some values or do something async in your page before convert it) you can enable the `waitForJS` local option, when `waitForJS` is set to true the conversion will wait until you set a variable to true in your page, by default the name of the variable is `ELECTRON_HTML_TO_READY` but you can customize it via `waitForJSVarName` option. 199 | 200 | Example 201 | ------- 202 | 203 | local options: 204 | 205 | ```js 206 | conversion({ 207 | html: '', 208 | waitForJS: true 209 | }, cb); 210 | ``` 211 | 212 | custom html: 213 | 214 | ```html 215 |

216 | 222 | ``` 223 | 224 | Debugging 225 | --------- 226 | 227 | - To get more information (internal debugging logs of the module) about what's happening inside the conversion run your app with the `DEBUG` env var: `DEBUG=electron-html-to,electron-html-to:* node app.js` (on Windows use `set DEBUG=electron-html-to,electron-html-to:* && node app.js`). This will print out some additional information about what's going on. 228 | 229 | - To see the electron process UI created (the visible electron window) and point stdout/stderr of the electron processes to console run your app with the `ELECTRON_HTML_TO_DEBUGGING` env var: `ELECTRON_HTML_TO_DEBUGGING=true node app.js` (on Windows use `set ELECTRON_HTML_TO_DEBUGGING=true && node app.js`). 230 | 231 | - To only point stdout/stderr of the electron processes to console run your app with the `ELECTRON_HTML_TO_STDSTREAMS` env var: `ELECTRON_HTML_TO_STDSTREAMS=true node app.js` (on Windows use `set ELECTRON_HTML_TO_STDSTREAMS=true && node app.js`). 232 | 233 | - To enable low level messages (chromium logs) of the electron processes run your app with the [`ELECTRON_ENABLE_LOGGING`](https://electron.atom.io/docs/api/chrome-command-line-switches/#enable-logging) env var: `ELECTRON_ENABLE_LOGGING=true node app.js` (on Windows use `set ELECTRON_ENABLE_LOGGING=true && node app.js`). 234 | 235 | Requirements 236 | ------------ 237 | 238 | - Install [electron](http://electron.atom.io/) >= 0.36.1 including electron@1, the easy way to install 239 | electron in your app is `npm install electron --save` or `npm install electron-prebuilt --save` 240 | 241 | Troubleshooting 242 | --------------- 243 | 244 | #### Using electron in single core machines 245 | 246 | If you are using a machine with a single-core processor you will probably experience a high CPU usage when doing any conversion (97% in most cases and the usage is worse when using Windows), this is because a limitation in electron when it is being used on single core machines, unfortunately the only way to overcome this is to upgrade your machine to a processor with more cores (a processor with two cores is fine). 247 | more info: [issue1](https://github.com/Microsoft/vscode/issues/17097), [issue2](https://github.com/Microsoft/vscode/issues/22724) 248 | 249 | #### env: node: No such file or directory when using electron-prebuilt and nvm 250 | 251 | If you are using node with [nvm](https://github.com/creationix/nvm) and you have installed electron with `npm install -g electron-prebuilt` you probably will see an error or log with `env: node: No such file or directory`, this is because the electron executable installed by `electron-prebuilt` is a node CLI spawning the real electron executable internally, since nvm don't install/symlink node to `/usr/bin/env/node` when the electron executable installed by `electron-prebuilt` tries to run, it will fail because `node` won't be found in that context.. 252 | 253 | Solution: 254 | 255 | 1.- Install `electron-prebuilt` as a dependency in your app, this is the option **recommended** because you probably want to ensure your app always run with the exact version you tested it, and probably you don't want to install electron globally in your system. 256 | 257 | 2.- You can make a symlink to `/usr/bin/env/node` but this is **not recommended** by nvm authors, because you will loose all the power that nvm brings. 258 | 259 | 3.- Put the path to the **real electron executable** in your `$PATH`. 260 | 261 | License 262 | ------- 263 | 264 | See [license](https://github.com/bjrmatos/electron-html-to/blob/master/LICENSE) 265 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | 2 | # Test against this version of Node.js 3 | environment: 4 | matrix: 5 | # node.js 6 | - nodejs_version: "0.10" 7 | - nodejs_version: "4.6" 8 | - nodejs_version: "6.9" 9 | 10 | # Install scripts. (runs after repo cloning) 11 | install: 12 | # Get the latest stable version of Node.js or io.js 13 | - ps: Install-Product node $env:nodejs_version 14 | # install modules 15 | - npm install 16 | 17 | # Post-install test scripts. 18 | test_script: 19 | # Output useful info for debugging. 20 | - node --version 21 | - npm --version 22 | # run tests 23 | - npm test 24 | 25 | # Don't actually build. 26 | build: off 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-html-to", 3 | "version": "2.6.1", 4 | "description": "Convert html to html/image using electron", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib", 8 | "build": "babel src --out-dir lib", 9 | "lint": "eslint src test", 10 | "test": "mocha test/test.js --timeout 8000", 11 | "prepublish": "in-publish && npm-run-all lint clean build || not-in-publish" 12 | }, 13 | "author": "BJR Matos (https://github.com/bjrmatos)", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/bjrmatos/electron-html-to.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/bjrmatos/electron-html-to/issues" 21 | }, 22 | "homepage": "https://github.com/bjrmatos/electron-html-to", 23 | "keywords": [ 24 | "html", 25 | "pdf", 26 | "image", 27 | "conversion", 28 | "electron" 29 | ], 30 | "dependencies": { 31 | "body": "5.1.0", 32 | "debug": "2.2.0", 33 | "electron-workers": "1.10.3", 34 | "lodash.pick": "4.2.1", 35 | "minstache": "1.2.0", 36 | "mkdirp": "0.5.1", 37 | "object-assign": "4.1.1", 38 | "pdfjs-dist": "1.5.285", 39 | "sliced": "1.0.1", 40 | "uuid": "2.0.2", 41 | "which": "1.2.9" 42 | }, 43 | "devDependencies": { 44 | "babel": "5.8.29", 45 | "electron": "1.6.6", 46 | "eslint": "2.11.1", 47 | "eslint-config-airbnb-base": "3.0.1", 48 | "eslint-plugin-import": "1.8.1", 49 | "in-publish": "2.0.0", 50 | "mocha": "2.5.3", 51 | "npm-run-all": "2.1.1", 52 | "rimraf": "2.5.2", 53 | "should": "9.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/conversion.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path'; 3 | import url from 'url'; 4 | import debug from 'debug'; 5 | import uuid from 'uuid'; 6 | import assign from 'object-assign'; 7 | import mkdirp from 'mkdirp'; 8 | import { name as pkgName } from '../package.json'; 9 | import saveFile from './saveFile'; 10 | import serverIpcStrategy from './serverIpcStrategy'; 11 | import dedicatedProcessStrategy from './dedicatedProcessStrategy'; 12 | 13 | const debugConversion = debug(`${pkgName}:conversion`); 14 | 15 | function writeHtmlFileIfNeccesary(opt, tmpPath, type, id, cb) { 16 | let htmlPath; 17 | 18 | if (!opt[type]) { 19 | return cb(); 20 | } 21 | 22 | htmlPath = path.resolve(path.join(tmpPath, `${id + type}.html`)); 23 | // eslint-disable-next-line no-param-reassign 24 | opt[`${type}File`] = path.resolve(htmlPath); 25 | 26 | debugConversion('creating temporal html file [type: %s] in %s..', type, htmlPath); 27 | saveFile(tmpPath, htmlPath, opt[type], cb); 28 | } 29 | 30 | function writeHtmlIfNeccesary(opt, tmpPath, id, cb) { 31 | debugConversion('creating temporal html files in %s..', tmpPath); 32 | 33 | writeHtmlFileIfNeccesary(opt, tmpPath, 'html', id, (htmlErr) => { 34 | if (htmlErr) { 35 | return cb(htmlErr); 36 | } 37 | 38 | writeHtmlFileIfNeccesary(opt, tmpPath, 'header', id, (headerErr) => { 39 | if (headerErr) { 40 | return cb(headerErr); 41 | } 42 | 43 | writeHtmlFileIfNeccesary(opt, tmpPath, 'footer', id, (footerErr) => { 44 | if (footerErr) { 45 | return cb(footerErr); 46 | } 47 | 48 | cb(); 49 | }); 50 | }); 51 | }); 52 | } 53 | 54 | function createConversion(options) { 55 | let mode; 56 | let serverIpcStrategyCall; 57 | 58 | if (options.strategy === 'electron-server') { 59 | mode = 'server'; 60 | } else if (options.strategy === 'electron-ipc') { 61 | mode = 'ipc'; 62 | } 63 | 64 | if (options.strategy === 'electron-server' || options.strategy === 'electron-ipc') { 65 | // each instance with ipc or server mode will create a new electron-workers instance. 66 | serverIpcStrategyCall = serverIpcStrategy(mode, options); 67 | } 68 | 69 | let conversion = (conversionOpts, cb) => { 70 | let localOpts = conversionOpts, 71 | converterPath, 72 | id; 73 | 74 | const conversionOptsDefault = { 75 | browserWindow: { 76 | webPreferences: {} 77 | }, 78 | waitForJSVarName: 'ELECTRON_HTML_TO_READY', 79 | collectLogs: true 80 | }; 81 | 82 | debugConversion('generating new conversion task..'); 83 | 84 | if (typeof conversionOpts === 'string' || conversionOpts instanceof String) { 85 | debugConversion('normalizing local options object from a plain string parameter: %s', conversionOpts); 86 | 87 | localOpts = { 88 | html: conversionOpts 89 | }; 90 | } 91 | 92 | localOpts = assign({}, conversionOptsDefault, localOpts); 93 | 94 | if (localOpts.converterPath) { 95 | converterPath = localOpts.converterPath; 96 | } else { 97 | converterPath = options.converterPath; 98 | } 99 | 100 | if (localOpts.waitForJS && localOpts.browserWindow.webPreferences && 101 | localOpts.browserWindow.webPreferences.javascript === false) { 102 | throw new Error('can\'t use waitForJS option if browserWindow["web-preferences"].javascript is not activated'); 103 | } 104 | 105 | id = uuid.v4(); 106 | debugConversion('conversion task id: %s', id); 107 | 108 | mkdirp(options.tmpDir, (mkdirErr) => { 109 | if (mkdirErr) { 110 | // eslint-disable-next-line no-param-reassign 111 | mkdirErr.message = `Error while trying to ensure tmpDir "${options.tmpDir}" existence: ${mkdirErr.message}`; 112 | return cb(mkdirErr); 113 | } 114 | 115 | writeHtmlIfNeccesary(localOpts, options.tmpDir, id, (err) => { 116 | if (err) { 117 | return cb(err); 118 | } 119 | 120 | // prefix the request in order to recognize later in electron protocol handler 121 | localOpts.url = localOpts.url || url.format({ 122 | protocol: 'file', 123 | pathname: localOpts.htmlFile 124 | }); 125 | 126 | localOpts.chromeCommandLineSwitches = options.chromeCommandLineSwitches; 127 | localOpts.extraHeaders = localOpts.extraHeaders || {}; 128 | 129 | localOpts.output = { 130 | tmpDir: path.resolve(path.join(options.tmpDir)), 131 | id 132 | }; 133 | 134 | delete localOpts.html; 135 | 136 | debugConversion('starting conversion task [strategy:%s][task id:%s] with options:', options.strategy, id, localOpts); 137 | 138 | if (options.strategy === 'electron-server' || options.strategy === 'electron-ipc') { 139 | return serverIpcStrategyCall(localOpts, converterPath, id, cb); 140 | } 141 | 142 | if (options.strategy === 'dedicated-process') { 143 | return dedicatedProcessStrategy(options, localOpts, converterPath, id, cb); 144 | } 145 | 146 | cb(new Error(`Unsupported strategy ${options.strategy}`)); 147 | }); 148 | }); 149 | }; 150 | 151 | function kill() { 152 | if (serverIpcStrategyCall) { 153 | serverIpcStrategyCall.kill(); 154 | } 155 | } 156 | 157 | conversion.options = options; 158 | conversion.kill = kill; 159 | 160 | return conversion; 161 | } 162 | 163 | export default createConversion; 164 | -------------------------------------------------------------------------------- /src/converters/pdf.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'), 3 | fs = require('fs'), 4 | assign = require('object-assign'), 5 | pdfParser = require('../pdfParser'); 6 | 7 | module.exports = function(log, settings, browserWindow, done) { 8 | let pdfDefaults = { 9 | marginsType: 0, 10 | pageSize: 'A4', 11 | printBackground: false, 12 | landscape: false 13 | }; 14 | 15 | // TODO: support headerHeight, footerHeight when electron support rendering PDF's header/footer 16 | let pdfSettings = settings.pdf, 17 | pdfOptions = assign({}, pdfDefaults, pdfSettings, { printSelectionOnly: false }); 18 | 19 | log('before printing..'); 20 | log('pdf options:', pdfOptions); 21 | 22 | browserWindow.webContents.printToPDF(pdfOptions, (err, pdfBuf) => { 23 | let dist = path.join(settings.output.tmpDir, `${settings.output.id}.pdf`); 24 | 25 | if (err) { 26 | return done(err); 27 | } 28 | 29 | // don't know why the electron process hangs up if i don't log anything here 30 | // (probably pdf.js?) 31 | // anyway this log prevent the conversion to stop 32 | log('after printing..'); 33 | log('parsing pdf..'); 34 | 35 | pdfParser(pdfBuf, (pdfParseErr, pdfDoc) => { 36 | log('pdf parsing complete..'); 37 | 38 | if (pdfParseErr) { 39 | return done(pdfParseErr); 40 | } 41 | 42 | // when running in IISNODE electron hangs when using fs.readFile, fs.createReadStream 43 | // or any async API for read a file.. on normal windows + node electron consumes 100% CPU when 44 | // using any async file API, so the only/best option is to read the file in a synchronous way 45 | if (process.platform === 'win32') { 46 | try { 47 | fs.writeFileSync(dist, pdfBuf); 48 | 49 | done(null, { 50 | numberOfPages: pdfDoc.numPages, 51 | output: dist 52 | }); 53 | } catch (saveErr) { 54 | done(saveErr); 55 | } 56 | } else { 57 | fs.writeFile(dist, pdfBuf, (saveErr) => { 58 | if (saveErr) { 59 | return done(saveErr); 60 | } 61 | 62 | done(null, { 63 | numberOfPages: pdfDoc.numPages, 64 | output: dist 65 | }); 66 | }); 67 | } 68 | }); 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /src/dedicatedProcessStrategy.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import childProcess from 'child_process'; 5 | import debug from 'debug'; 6 | import which from 'which'; 7 | import sliced from 'sliced'; 8 | import ipc from './ipc'; 9 | import saveFile from './saveFile'; 10 | import { name as pkgName } from '../package.json'; 11 | 12 | const debugStrategy = debug(`${pkgName}:dedicated-process-strategy`), 13 | debugElectronLog = debug(`${pkgName}:electron-log`), 14 | debugPage = debug(`${pkgName}:page`); 15 | 16 | let ELECTRON_PATH; 17 | 18 | function getElectronPath() { 19 | let electron; 20 | 21 | if (ELECTRON_PATH) { 22 | debugStrategy('getting electron path from cache'); 23 | return ELECTRON_PATH; 24 | } 25 | 26 | // first try to find the electron executable if it is installed from `electron`.. 27 | electron = getElectronPathFromPackage('electron'); 28 | 29 | if (electron == null) { 30 | // second try to find the electron executable if it is installed from `electron-prebuilt`.. 31 | electron = getElectronPathFromPackage('electron-prebuilt'); 32 | } 33 | 34 | if (electron == null) { 35 | // last try to find the electron executable, trying using which module 36 | debugStrategy('trying to get electron path from $PATH..'); 37 | 38 | try { 39 | electron = which.sync('electron'); 40 | } catch (whichErr) { 41 | throw new Error( 42 | 'Couldn\'t find the path to the electron executable automatically, ' + 43 | 'try installing the `electron` or `electron-prebuilt` package, ' + 44 | 'or set the `pathToElectron` option to specify the path manually' 45 | ); 46 | } 47 | } 48 | 49 | ELECTRON_PATH = electron; 50 | 51 | return electron; 52 | } 53 | 54 | function getElectronPathFromPackage(moduleName) { 55 | let electronPath; 56 | 57 | try { 58 | debugStrategy(`trying to get electron path from "${moduleName}" module..`); 59 | 60 | // eslint-disable-next-line global-require 61 | electronPath = require(moduleName); 62 | 63 | return electronPath; 64 | } catch (err) { 65 | if (err.code === 'MODULE_NOT_FOUND') { 66 | return electronPath; 67 | } 68 | 69 | throw err; 70 | } 71 | } 72 | 73 | export default function(options, requestOptions, converterPath, id, cb) { 74 | const { 75 | tmpDir, 76 | timeout, 77 | pathToElectron, 78 | electronArgs, 79 | allowLocalFilesAccess, 80 | maxLogEntrySize 81 | } = options; 82 | 83 | const settingsFilePath = path.resolve(path.join(tmpDir, `${id}settings.html`)); 84 | const settingsContent = JSON.stringify({ ...requestOptions, converterPath, allowLocalFilesAccess, maxLogEntrySize }); 85 | 86 | debugStrategy('saving settings in temporal file..'); 87 | 88 | saveFile(tmpDir, settingsFilePath, settingsContent, (saveFileErr) => { 89 | let childArgs = []; 90 | 91 | let debugMode = false, 92 | isDone = false, 93 | electronPath, 94 | childOpts, 95 | child, 96 | childIpc, 97 | timeoutId; 98 | 99 | if (saveFileErr) { 100 | return cb(saveFileErr); 101 | } 102 | 103 | if (Array.isArray(electronArgs)) { 104 | childArgs = electronArgs.slice(); 105 | } 106 | 107 | childArgs.unshift(path.join(__dirname, 'scripts', 'standaloneScript.js')); 108 | 109 | childOpts = { 110 | env: { 111 | ELECTRON_WORKER_ID: id, 112 | ELECTRON_HTML_TO_SETTINGS_FILE_PATH: settingsFilePath, 113 | // propagate the DISPLAY env var to make it work on LINUX 114 | DISPLAY: process.env.DISPLAY 115 | }, 116 | stdio: [null, null, null, 'ipc'] 117 | }; 118 | 119 | debugStrategy('searching electron executable path..'); 120 | 121 | if (pathToElectron) { 122 | debugStrategy('using electron executable path from custom location: %s', pathToElectron); 123 | } 124 | 125 | electronPath = pathToElectron || getElectronPath(); 126 | 127 | if (process.env.ELECTRON_HTML_TO_DEBUGGING !== undefined) { 128 | debugStrategy('electron process debugging mode activated'); 129 | debugMode = true; 130 | childOpts.env.ELECTRON_HTML_TO_DEBUGGING = process.env.ELECTRON_HTML_TO_DEBUGGING; 131 | } 132 | 133 | if (process.env.ELECTRON_ENABLE_LOGGING !== undefined) { 134 | childOpts.env.ELECTRON_ENABLE_LOGGING = process.env.ELECTRON_ENABLE_LOGGING; 135 | } 136 | 137 | if (process.env.IISNODE_VERSION !== undefined) { 138 | debugStrategy('running in IISNODE..'); 139 | childOpts.env.IISNODE_VERSION = process.env.IISNODE_VERSION; 140 | } 141 | 142 | if (debugMode || process.env.ELECTRON_ENABLE_LOGGING !== undefined || process.env.ELECTRON_HTML_TO_STDSTREAMS !== undefined) { 143 | childOpts.stdio = [null, process.stdout, process.stderr, 'ipc']; 144 | } 145 | 146 | debugStrategy('spawning new electron process with args:', childArgs, 'and options:', childOpts); 147 | 148 | child = childProcess.spawn(electronPath, childArgs, childOpts); 149 | 150 | debugStrategy('electron process pid:', child.pid); 151 | 152 | childIpc = ipc(child); 153 | 154 | debugStrategy('processing conversion..'); 155 | 156 | child.on('exit', (code, signal) => { 157 | debugStrategy(`electron process exit with code: ${code} and signal: ${signal}`); 158 | }); 159 | 160 | child.on('error', (err) => { 161 | if (isDone) { 162 | return; 163 | } 164 | 165 | isDone = true; 166 | 167 | debugStrategy('electron process has an error: %s', err.message); 168 | 169 | cb(err); 170 | clearTimeout(timeoutId); 171 | 172 | if (child.connected) { 173 | child.disconnect(); 174 | } 175 | 176 | child.kill(); 177 | }); 178 | 179 | childIpc.on('page-error', (windowId, errMsg, errStack) => { 180 | debugPage('An error has ocurred in browser window [%s]: message: %s stack: %s', windowId, errMsg, errStack); 181 | }); 182 | 183 | childIpc.on('page-log', function() { 184 | // eslint-disable-next-line prefer-rest-params 185 | let newArgs = sliced(arguments), 186 | windowId = newArgs.splice(0, 1); 187 | 188 | newArgs.unshift(`console log from browser window [${windowId}]:`); 189 | debugPage.apply(debugPage, newArgs); 190 | }); 191 | 192 | childIpc.on('log', function() { 193 | // eslint-disable-next-line prefer-rest-params 194 | debugElectronLog.apply(debugElectronLog, sliced(arguments)); 195 | }); 196 | 197 | childIpc.once('finish', (err, childData) => { 198 | if (isDone) { 199 | return; 200 | } 201 | 202 | isDone = true; 203 | clearTimeout(timeoutId); 204 | 205 | if (err) { 206 | debugStrategy('conversion ended with error..'); 207 | cb(new Error(err)); 208 | } else { 209 | // disabling no-undef rule because eslint don't detect object rest spread correctly 210 | /* eslint-disable no-undef */ 211 | let { output, ...restData } = childData; 212 | 213 | debugStrategy('conversion ended successfully..'); 214 | 215 | if (Array.isArray(restData.logs)) { 216 | restData.logs.forEach((m) => { 217 | // eslint-disable-next-line no-param-reassign 218 | m.timestamp = new Date(m.timestamp); 219 | }); 220 | } 221 | 222 | cb(null, { 223 | ...restData, 224 | stream: fs.createReadStream(output) 225 | }); 226 | /* eslint-enable no-undef */ 227 | } 228 | 229 | // in debug mode, don't close the electron process 230 | if (!debugMode) { 231 | if (child.connected) { 232 | child.disconnect(); 233 | } 234 | 235 | child.kill(); 236 | } 237 | }); 238 | 239 | timeoutId = setTimeout(() => { 240 | let timeoutErr; 241 | 242 | if (isDone) { 243 | return; 244 | } 245 | 246 | debugStrategy('conversion timeout..'); 247 | 248 | isDone = true; 249 | timeoutErr = new Error('Timeout when executing in electron'); 250 | timeoutErr.electronTimeout = true; 251 | 252 | cb(timeoutErr); 253 | 254 | if (child.connected) { 255 | child.disconnect(); 256 | } 257 | 258 | child.kill(); 259 | }, requestOptions.timeout || timeout); 260 | }); 261 | } 262 | -------------------------------------------------------------------------------- /src/ensureStartWorker.js: -------------------------------------------------------------------------------- 1 | 2 | import debug from 'debug'; 3 | import sliced from 'sliced'; 4 | import ipc from './ipc'; 5 | import { name as pkgName } from '../package.json'; 6 | 7 | const debugPage = debug(`${pkgName}:page`), 8 | debugElectronLog = debug(`${pkgName}:electron-log`); 9 | 10 | function listenLog(debugStrategy, worker, workerProcess) { 11 | let workerIpc = ipc(workerProcess); 12 | 13 | debugStrategy(`establishing listeners for electron logs in worker [${worker.id}]..`); 14 | 15 | workerIpc.on('page-error', (windowId, errMsg, errStack) => { 16 | debugPage('An error has ocurred in browser window [%s]: message: %s stack: %s', windowId, errMsg, errStack); 17 | }); 18 | 19 | workerIpc.on('page-log', function() { 20 | // eslint-disable-next-line prefer-rest-params 21 | let newArgs = sliced(arguments), 22 | windowId = newArgs.splice(0, 1); 23 | 24 | newArgs.unshift(`console log from browser window [${windowId}]:`); 25 | debugPage.apply(debugPage, newArgs); 26 | }); 27 | 28 | workerIpc.on('log', function() { 29 | // eslint-disable-next-line prefer-rest-params 30 | debugElectronLog.apply(debugElectronLog, sliced(arguments)); 31 | }); 32 | } 33 | 34 | export default function ensureStart(debugStrategy, workers, instance, cb) { 35 | if (instance.started) { 36 | return cb(); 37 | } 38 | 39 | instance.startCb.push(cb); 40 | 41 | if (instance.starting) { 42 | return; 43 | } 44 | 45 | debugStrategy('starting electron workers..'); 46 | 47 | // eslint-disable-next-line no-param-reassign 48 | instance.starting = true; 49 | 50 | workers.on('workerProcessCreated', (worker, workerProcess) => { 51 | listenLog(debugStrategy, worker, workerProcess); 52 | }); 53 | 54 | workers.start((startErr) => { 55 | // eslint-disable-next-line no-param-reassign 56 | instance.starting = false; 57 | 58 | if (startErr) { 59 | let startCbs = instance.startCb.slice(0); 60 | 61 | // clean up callback queue when workers fail to start, necessary to prevent 62 | // duplication of callback invocation when trying to call `conversion` again after a failing start of workers 63 | instance.startCb.splice(0, instance.startCb.length); 64 | 65 | startCbs.forEach((callback) => callback(startErr)); 66 | return; 67 | } 68 | 69 | debugStrategy('electron workers started successfully..'); 70 | 71 | // eslint-disable-next-line no-param-reassign 72 | instance.started = true; 73 | instance.startCb.forEach((callback) => callback()); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import os from 'os'; 3 | import path from 'path'; 4 | import debug from 'debug'; 5 | import assign from 'object-assign'; 6 | import { name as pkgName } from '../package.json'; 7 | import createConversion from './conversion'; 8 | 9 | const debugMe = debug(pkgName); 10 | 11 | function conversionFactory(userOptions = {}) { 12 | let conversion; 13 | 14 | const optionsDefault = { 15 | timeout: 10000, 16 | numberOfWorkers: 2, 17 | chromeCommandLineSwitches: {}, 18 | allowLocalFilesAccess: false, 19 | // default size for console.log messages 20 | maxLogEntrySize: 1000, 21 | // namespace for tmp dir 22 | tmpDir: path.join(os.tmpdir(), `${pkgName}-tmp-data`), 23 | strategy: 'electron-ipc' 24 | }; 25 | 26 | const options = assign({}, optionsDefault, userOptions); 27 | 28 | if (Object.keys(options.chromeCommandLineSwitches).length === 0) { 29 | options.chromeCommandLineSwitches['ignore-certificate-errors'] = null; 30 | } 31 | 32 | debugMe('Creating a new conversion function with options:', options); 33 | 34 | // always set env var names for electron-workers (don't let the user override this config) 35 | options.hostEnvVarName = 'ELECTRON_WORKER_HOST'; 36 | options.portEnvVarName = 'ELECTRON_WORKER_PORT'; 37 | 38 | conversion = createConversion(options); 39 | 40 | return conversion; 41 | } 42 | 43 | conversionFactory.converters = {}; 44 | conversionFactory.converters.PDF = path.resolve(__dirname, './converters/pdf.js'); 45 | 46 | export default conversionFactory; 47 | -------------------------------------------------------------------------------- /src/ipc.js: -------------------------------------------------------------------------------- 1 | 2 | const EventEmitter = require('events').EventEmitter, 3 | sliced = require('sliced'); 4 | 5 | function IPC(processObj) { 6 | const emitter = new EventEmitter(), 7 | emit = emitter.emit; 8 | 9 | // no parent 10 | if (!processObj.send) { 11 | return emitter; 12 | } 13 | 14 | processObj.on('message', (data) => { 15 | let shouldIgnore = false; 16 | 17 | // not handling electron-workers events here 18 | if (data) { 19 | shouldIgnore = data.workerEvent === 'ping' || data.workerEvent === 'pong' || data.workerEvent === 'task'; 20 | 21 | if (shouldIgnore) { 22 | return; 23 | } 24 | } 25 | 26 | emit.apply(emitter, sliced(data)); 27 | }); 28 | 29 | emitter.emit = function() { 30 | if (processObj.connected) { 31 | // eslint-disable-next-line prefer-rest-params 32 | processObj.send(sliced(arguments)); 33 | } 34 | }; 35 | 36 | return emitter; 37 | } 38 | 39 | module.exports = IPC; 40 | -------------------------------------------------------------------------------- /src/pdfParser/index.js: -------------------------------------------------------------------------------- 1 | 2 | let pdfjs = require('pdfjs-dist/build/pdf'); 3 | 4 | module.exports = function parsePDF(pdfBuf, cb) { 5 | let pdfData; 6 | 7 | try { 8 | pdfData = new Uint8Array(pdfBuf); 9 | 10 | pdfjs.getDocument(pdfData).then((doc) => { 11 | cb(null, doc); 12 | }).catch((err) => { 13 | cb(err); 14 | }); 15 | } catch (uncaughtErr) { 16 | cb(uncaughtErr); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/saveFile.js: -------------------------------------------------------------------------------- 1 | 2 | import fs from 'fs'; 3 | import mkdirp from 'mkdirp'; 4 | 5 | export default function saveFile(dirPath, filePath, content, cb) { 6 | mkdirp(dirPath, (err) => { 7 | if (err) { 8 | return cb(err); 9 | } 10 | 11 | fs.writeFile(filePath, content, cb); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/scripts/conversionScript.js: -------------------------------------------------------------------------------- 1 | 2 | // disabling import rule because `electron` is a built-in module 3 | // eslint-disable-next-line import/no-unresolved 4 | const renderer = require('electron').ipcMain, 5 | assign = require('object-assign'); 6 | 7 | module.exports = function(settings, browserWindow, evaluate, log, converter, respond) { 8 | let pageJSisDone = !Boolean(settings.waitForJS); 9 | 10 | renderer.once(`${browserWindow.id}:waitForJS`, () => { 11 | log('waitForJS signal received..'); 12 | pageJSisDone = true; 13 | }); 14 | 15 | browserWindow.webContents.on('did-finish-load', () => { 16 | log('browser window loaded..'); 17 | 18 | if (settings.browserWindow.webPreferences.javascript === false) { 19 | log('javascript is disabled for the page..'); 20 | 21 | next(); 22 | } else { 23 | evaluate(() => { 24 | const sElectronHeader = '#electronHeader', 25 | sElectronFooter = '#electronFooter'; 26 | 27 | return { 28 | electronHeader: document.querySelector(sElectronHeader) ? document.querySelector(sElectronHeader).innerHTML : null, 29 | electronFooter: document.querySelector(sElectronFooter) ? document.querySelector(sElectronFooter).innerHTML : null 30 | }; 31 | }, (err, extraContent) => { 32 | if (err) { 33 | return respond(err); 34 | } 35 | 36 | next(extraContent); 37 | }); 38 | } 39 | 40 | function next(extraContent) { 41 | /* eslint no-unused-vars: [0] */ 42 | // TODO: ask support for header/footer pdf and numberOfPages in electron 43 | log('waiting for browser window resolution..'); 44 | 45 | setTimeout(() => { 46 | resolvePage(); 47 | }, settings.delay || 0); 48 | } 49 | 50 | function resolvePage() { 51 | if (settings.waitForJS && !pageJSisDone) { 52 | setTimeout(() => { 53 | resolvePage(); 54 | }, 100); 55 | 56 | return; 57 | } 58 | 59 | log('calling converter function..'); 60 | 61 | converter(log, assign({}, settings), browserWindow, (converterErr, data) => { 62 | log('converter function ended..'); 63 | respond(converterErr, data); 64 | }); 65 | } 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /src/scripts/evaluateJS.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | var renderer = require('electron').ipcMain, 5 | sliced = require('sliced'), 6 | evaluateTemplate = require('./evaluateTemplate'); 7 | 8 | function evaluate(bWindow, src, done) { 9 | var id = bWindow.id; 10 | 11 | renderer.once(`${id}:evaluateResponse`, (ev, response) => { 12 | done(null, response); 13 | }); 14 | 15 | renderer.once(`${id}:evaluateError`, (ev, error) => { 16 | done(error); 17 | }); 18 | 19 | bWindow.webContents.executeJavaScript(src); 20 | } 21 | 22 | module.exports = function(bWindow) { 23 | return function(fn/**, arg1, arg2..., done**/) { 24 | var args = sliced(arguments), 25 | done = args[args.length - 1], 26 | newArgs = args.slice(1, -1), 27 | src; 28 | 29 | src = evaluateTemplate.execute({ 30 | id: bWindow.id, 31 | src: String(fn), 32 | args: newArgs 33 | }); 34 | 35 | evaluate(bWindow, src, done); 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/scripts/evaluateTemplate.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | var minstache = require('minstache'); 5 | 6 | /** 7 | * Run the `src` function on the client-side (renderer-process), capture 8 | * the response, and send back via 9 | * ipc to electron's main process 10 | */ 11 | 12 | var execute = [ 13 | "(function evaluateJavaScript() {", 14 | " var ipc = __electron_html_to.ipc;", 15 | " var sliced = __electron_html_to.sliced;", 16 | " ipc.send('log', {{id}}, 'evaluating javascript in page..');", 17 | " try {", 18 | " var response = ({{!src}})({{!args}})", 19 | " ipc.send('{{id}}:evaluateResponse', response);", 20 | " } catch (e) {", 21 | " ipc.send('{{id}}:evaluateError', e.message);", 22 | " }", 23 | "})()" 24 | ].join('\n'); 25 | 26 | exports.execute = minstache.compile(execute); 27 | -------------------------------------------------------------------------------- /src/scripts/getBrowserWindowOpts.js: -------------------------------------------------------------------------------- 1 | /* eslint no-var: [0] */ 2 | 3 | var path = require('path'), 4 | assign = require('object-assign'), 5 | pick = require('lodash.pick'); 6 | 7 | var browserWindowDefaults = { 8 | width: 600, 9 | height: 600 10 | }; 11 | 12 | var webPreferencesDefaults = { 13 | nodeIntegration: false, 14 | javascript: true, 15 | webSecurity: false 16 | }; 17 | 18 | module.exports = function(browserWindowSettings) { 19 | var browserWindowOpts, 20 | webPreferences; 21 | 22 | browserWindowOpts = pick(browserWindowSettings || {}, [ 23 | 'width', 24 | 'height', 25 | 'x', 26 | 'y', 27 | 'useContentSize', 28 | 'webPreferences' 29 | ]); 30 | 31 | browserWindowOpts = assign({}, browserWindowDefaults, browserWindowOpts, { 32 | show: false 33 | }); 34 | 35 | webPreferences = pick(browserWindowOpts.webPreferences || {}, [ 36 | 'nodeIntegration', 37 | 'partition', 38 | 'zoomFactor', 39 | 'javascript', 40 | 'webSecurity', 41 | 'allowDisplayingInsecureContent', 42 | 'allowRunningInsecureContent', 43 | 'images', 44 | 'java', 45 | 'webgl', 46 | 'webaudio', 47 | 'plugins', 48 | 'experimentalFeatures', 49 | 'experimentalCanvasFeatures', 50 | 'overlayScrollbars', 51 | 'overlayFullscreenVideo', 52 | 'sharedWorker', 53 | 'directWrite' 54 | ]); 55 | 56 | browserWindowOpts.webPreferences = assign({}, webPreferencesDefaults, webPreferences, { 57 | preload: path.join(__dirname, 'preload.js') 58 | }); 59 | 60 | return browserWindowOpts; 61 | }; 62 | -------------------------------------------------------------------------------- /src/scripts/ipcScript.js: -------------------------------------------------------------------------------- 1 | 2 | const util = require('util'), 3 | // disabling import rule because `electron` is a built-in module 4 | // eslint-disable-next-line import/no-unresolved 5 | electron = require('electron'), 6 | sliced = require('sliced'), 7 | assign = require('object-assign'), 8 | getBrowserWindowOpts = require('./getBrowserWindowOpts'), 9 | listenRequestsInPage = require('./listenRequestsInPage'), 10 | conversionScript = require('./conversionScript'), 11 | evaluate = require('./evaluateJS'), 12 | parentChannel = require('../ipc')(process), 13 | app = electron.app, 14 | renderer = electron.ipcMain, 15 | BrowserWindow = electron.BrowserWindow; 16 | 17 | let windows = [], 18 | electronVersion, 19 | log, 20 | WORKER_ID, 21 | DEBUG_MODE, 22 | CHROME_COMMAND_LINE_SWITCHES, 23 | ALLOW_LOCAL_FILES_ACCESS, 24 | MAX_LOG_ENTRY_SIZE; 25 | 26 | WORKER_ID = process.env.ELECTRON_WORKER_ID; 27 | DEBUG_MODE = Boolean(process.env.ELECTRON_HTML_TO_DEBUGGING); 28 | CHROME_COMMAND_LINE_SWITCHES = JSON.parse(process.env.chromeCommandLineSwitches); 29 | ALLOW_LOCAL_FILES_ACCESS = process.env.allowLocalFilesAccess === 'true'; 30 | MAX_LOG_ENTRY_SIZE = parseInt(process.env.maxLogEntrySize, 10); 31 | 32 | if (process.versions.electron) { 33 | electronVersion = process.versions.electron; 34 | } else if (process.versions['atom-shell']) { 35 | electronVersion = process.versions['atom-shell']; 36 | } else { 37 | electronVersion = ''; 38 | } 39 | 40 | if (isNaN(MAX_LOG_ENTRY_SIZE)) { 41 | MAX_LOG_ENTRY_SIZE = 1000; 42 | } 43 | 44 | log = function() { 45 | // eslint-disable-next-line prefer-rest-params 46 | let newArgs = sliced(arguments); 47 | 48 | newArgs.unshift(`[Worker ${WORKER_ID}]`); 49 | 50 | parentChannel.emit.apply(parentChannel, ['log'].concat(newArgs)); 51 | }; 52 | 53 | global.windowsData = {}; 54 | global.windowsLogs = {}; 55 | 56 | Object.keys(CHROME_COMMAND_LINE_SWITCHES).forEach((switchName) => { 57 | let switchValue = CHROME_COMMAND_LINE_SWITCHES[switchName]; 58 | 59 | if (switchValue != null) { 60 | log(`establishing chrome command line switch [${switchName}:${switchValue}]`); 61 | app.commandLine.appendSwitch(switchName, switchValue); 62 | } else { 63 | log(`establishing chrome command line switch [${switchName}]`); 64 | app.commandLine.appendSwitch(switchName); 65 | } 66 | }); 67 | 68 | if (app.dock && typeof app.dock.hide === 'function') { 69 | if (!DEBUG_MODE) { 70 | app.dock.hide(); 71 | } 72 | } 73 | 74 | app.on('window-all-closed', () => { 75 | // by default dont close the app (because the electron ipc will be running) 76 | // only close when debug mode is on 77 | if (DEBUG_MODE) { 78 | app.quit(); 79 | } 80 | }); 81 | 82 | app.on('ready', () => { 83 | log('electron process ready..'); 84 | 85 | renderer.on('page-error', (ev, windowId, errMsg, errStack) => { 86 | // saving errors on page 87 | saveLogsInStore(global.windowsLogs[windowId], 'warn', `error in page: ${errMsg}`); 88 | 89 | saveLogsInStore(global.windowsLogs[windowId], 'warn', `error in page stack: ${errStack}`); 90 | 91 | parentChannel.emit('page-error', windowId, errMsg, errStack); 92 | }); 93 | 94 | renderer.on('page-log', (ev, args) => { 95 | let windowId = args[0], 96 | logLevel = args[1], 97 | logArgs = args.slice(2), 98 | // removing log level argument 99 | newArgs = args.slice(0, 1).concat(logArgs); 100 | 101 | // saving logs 102 | saveLogsInStore(global.windowsLogs[windowId], logLevel, logArgs, true); 103 | 104 | parentChannel.emit.apply(parentChannel, ['page-log'].concat(newArgs)); 105 | }); 106 | 107 | renderer.on('log', function() { 108 | // eslint-disable-next-line prefer-rest-params 109 | let newArgs = sliced(arguments), 110 | windowId = newArgs.splice(0, 2)[1]; 111 | 112 | newArgs.unshift(`[Browser window - ${windowId} log ]:`); 113 | 114 | log.apply(log, newArgs); 115 | }); 116 | 117 | // communication with electron-workers ipc 118 | process.on('message', (data) => { 119 | if (!data) { 120 | return; 121 | } 122 | 123 | function respondIpc(err, payload) { 124 | let msg = { 125 | workerEvent: 'taskResponse', 126 | taskId: data.taskId 127 | }; 128 | 129 | if (err) { 130 | // err.message is lost when serialized, so we need to explicitly set it 131 | msg.error = assign({}, err); 132 | msg.error.message = err.message; 133 | } else { 134 | msg.response = payload; 135 | } 136 | 137 | process.send(msg); 138 | } 139 | 140 | if (data.workerEvent === 'ping') { 141 | process.send({ workerEvent: 'pong' }); 142 | } else if (data.workerEvent === 'task') { 143 | log('new task for electron-ipc..'); 144 | 145 | try { 146 | createBrowserWindow(respondIpc, data.payload); 147 | } catch (uncaughtErr) { 148 | respondIpc(uncaughtErr); 149 | } 150 | } 151 | }); 152 | }); 153 | 154 | function createBrowserWindow(respondIpc, settingsData) { 155 | let evaluateInWindow, 156 | dataForWindow = {}, 157 | browserWindowOpts, 158 | converterPath, 159 | converter, 160 | currentWindow, 161 | currentWindowId, 162 | extraHeaders = ''; 163 | 164 | function respond(err, data) { 165 | log('finishing work in browser-window..'); 166 | 167 | if (err) { 168 | return respondIpc(err); 169 | } 170 | 171 | if (settingsData.collectLogs) { 172 | // eslint-disable-next-line no-param-reassign 173 | data.logs = global.windowsLogs[currentWindowId]; 174 | } else { 175 | // eslint-disable-next-line no-param-reassign 176 | data.logs = []; 177 | } 178 | 179 | respondIpc(null, data); 180 | 181 | if (!currentWindow) { 182 | return; 183 | } 184 | 185 | // in debug mode, don't destroy the browser window 186 | if (!DEBUG_MODE) { 187 | log('destroying browser window..'); 188 | currentWindow.destroy(); 189 | } 190 | } 191 | 192 | converterPath = settingsData.converterPath; 193 | 194 | log(`requiring converter module from ${converterPath}`); 195 | 196 | try { 197 | // eslint-disable-next-line global-require 198 | converter = require(converterPath); 199 | } catch (requireErr) { 200 | return respond(requireErr); 201 | } 202 | 203 | if (settingsData.waitForJS) { 204 | log('waitForJS enabled..'); 205 | 206 | dataForWindow.waitForJS = settingsData.waitForJS; 207 | dataForWindow.waitForJSVarName = settingsData.waitForJSVarName; 208 | } 209 | 210 | // get browser window options with defaults 211 | browserWindowOpts = getBrowserWindowOpts(settingsData.browserWindow); 212 | 213 | log('creating new browser window with options:', browserWindowOpts); 214 | 215 | if (DEBUG_MODE) { 216 | browserWindowOpts.show = true; 217 | } 218 | 219 | if (browserWindowOpts.show) { 220 | log('browser window visibility activated'); 221 | } 222 | 223 | currentWindow = new BrowserWindow(browserWindowOpts); 224 | currentWindowId = currentWindow.id; 225 | addWindow(currentWindow); 226 | 227 | evaluateInWindow = evaluate(currentWindow); 228 | global.windowsData[currentWindowId] = dataForWindow; 229 | global.windowsLogs[currentWindowId] = []; 230 | 231 | saveLogsInStore( 232 | global.windowsLogs[currentWindowId], 233 | 'debug', 234 | `Converting using electron-ipc strategy in electron ${electronVersion}` 235 | ); 236 | 237 | currentWindow.webContents.setAudioMuted(true); 238 | 239 | listenRequestsInPage( 240 | currentWindow, 241 | { 242 | allowLocalFilesAccess: ALLOW_LOCAL_FILES_ACCESS, 243 | pageUrl: settingsData.url 244 | }, 245 | log, 246 | saveLogsInStore(global.windowsLogs[currentWindowId]) 247 | ); 248 | 249 | currentWindow.on('closed', () => { 250 | log('browser-window closed..'); 251 | 252 | delete global.windowsData[currentWindowId]; 253 | delete global.windowsLogs[currentWindowId]; 254 | 255 | removeWindow(currentWindow); 256 | currentWindow = null; 257 | }); 258 | 259 | conversionScript(settingsData, currentWindow, evaluateInWindow, log, converter, respond); 260 | 261 | if (settingsData.userAgent) { 262 | log(`setting up custom user agent: ${settingsData.userAgent}`); 263 | currentWindow.webContents.setUserAgent(settingsData.userAgent); 264 | } 265 | 266 | if (typeof settingsData.extraHeaders === 'object') { 267 | Object.keys(settingsData.extraHeaders).forEach((key) => { 268 | extraHeaders += `${key}: ${settingsData.extraHeaders[key]}\n`; 269 | }); 270 | } 271 | 272 | log(util.format('loading url in browser window: %s, with headers: %s', settingsData.url, extraHeaders)); 273 | 274 | if (extraHeaders) { 275 | currentWindow.loadURL(settingsData.url, { 276 | extraHeaders 277 | }); 278 | } else { 279 | currentWindow.loadURL(settingsData.url); 280 | } 281 | 282 | // useful in windows to prevent the electron process to hang.. 283 | currentWindow.focus(); 284 | } 285 | 286 | function addWindow(browserWindow) { 287 | windows.push(browserWindow); 288 | } 289 | 290 | function removeWindow(browserWindow) { 291 | windows.forEach((win, index) => { 292 | if (win === browserWindow) { 293 | windows.splice(index, 1); 294 | } 295 | }); 296 | } 297 | 298 | function saveLogsInStore(store, level, msg, userLevel = false) { 299 | // eslint-disable-next-line prefer-rest-params 300 | let args = sliced(arguments); 301 | 302 | if (args.length === 1) { 303 | return _saveLogs.bind(undefined, store); 304 | } 305 | 306 | return _saveLogs(store, level, msg, userLevel); 307 | 308 | function _saveLogs(_store, _level, _msg, _userLevel) { 309 | const meta = { 310 | level: _level, 311 | message: trimMessage(_msg), 312 | timestamp: new Date().getTime() 313 | }; 314 | 315 | if (_userLevel) { 316 | meta.userLevel = true; 317 | } 318 | 319 | _store.push(meta); 320 | } 321 | } 322 | 323 | function trimMessage(args) { 324 | let message = args; 325 | 326 | if (Array.isArray(args)) { 327 | message = args.join(' '); 328 | } 329 | 330 | if (message.length > MAX_LOG_ENTRY_SIZE) { 331 | return `${message.substring(0, MAX_LOG_ENTRY_SIZE)}...`; 332 | } 333 | 334 | return message; 335 | } 336 | -------------------------------------------------------------------------------- /src/scripts/listenRequestsInPage.js: -------------------------------------------------------------------------------- 1 | 2 | const urlModule = require('url'); 3 | 4 | module.exports = function(browserWindow, options, log, saveLogInPage) { 5 | let allowLocalFilesAccess = options.allowLocalFilesAccess, 6 | parsedPageUrl = urlModule.parse(options.pageUrl), 7 | pageProtocol, 8 | redirectURL, 9 | pageRequested = false; 10 | 11 | if (!parsedPageUrl.protocol) { 12 | pageProtocol = 'file'; 13 | } else { 14 | // removing ':' 15 | pageProtocol = parsedPageUrl.protocol.slice(0, -1); 16 | } 17 | 18 | browserWindow.webContents.session.webRequest.onBeforeRequest((details, cb) => { 19 | let resourceUrl = details.url, 20 | msg; 21 | 22 | if (!pageRequested) { 23 | msg = `request to load the page: ${resourceUrl}`; 24 | pageRequested = true; 25 | 26 | log(msg); 27 | 28 | saveLogInPage('debug', msg); 29 | 30 | return cb({ cancel: false }); 31 | } 32 | 33 | msg = `request for resource: ${resourceUrl}, resourceType: ${details.resourceType}`; 34 | 35 | log(msg); 36 | 37 | saveLogInPage('debug', msg); 38 | 39 | if (resourceUrl.lastIndexOf('file:///', 0) === 0 && !allowLocalFilesAccess) { 40 | // potentially dangerous request 41 | msg = `denying request to a file because local file access is disabled, url: ${resourceUrl}`; 42 | 43 | log(msg); 44 | 45 | saveLogInPage('warn', msg); 46 | 47 | return cb({ cancel: true }); 48 | } else if (resourceUrl.lastIndexOf('file://', 0) === 0 && resourceUrl.lastIndexOf('file:///', 0) !== 0) { 49 | // support cdn like format -> //cdn.jquery... 50 | if (pageProtocol === 'file') { 51 | redirectURL = `http://${resourceUrl.substr(7)}`; 52 | } else { 53 | redirectURL = `${pageProtocol}://${resourceUrl.substr(7)}`; 54 | } 55 | 56 | msg = `handling cdn format request, url: ${resourceUrl.substr(7)}, redirecting to: ${redirectURL}`; 57 | 58 | log(msg); 59 | 60 | saveLogInPage('debug', msg); 61 | 62 | return cb({ 63 | cancel: false, 64 | redirectURL 65 | }); 66 | } 67 | 68 | cb({ cancel: false }); 69 | }); 70 | 71 | browserWindow.webContents.session.webRequest.onCompleted((details) => { 72 | let msg = ( 73 | `request for resource completed: ${details.url}, resourceType: ${details.resourceType}, status: ${details.statusCode}` 74 | ); 75 | 76 | log(msg); 77 | 78 | saveLogInPage('debug', msg); 79 | }); 80 | 81 | browserWindow.webContents.session.webRequest.onErrorOccurred((details) => { 82 | let msg = ( 83 | `request for resource failed: ${details.url}, resourceType: ${details.resourceType}, error: ${details.error}` 84 | ); 85 | 86 | log(msg); 87 | 88 | saveLogInPage('warn', msg); 89 | }); 90 | }; 91 | -------------------------------------------------------------------------------- /src/scripts/preload.js: -------------------------------------------------------------------------------- 1 | 2 | (function() { 3 | /* eslint-disable */ 4 | window.__electron_html_to = {}; 5 | 6 | var remote = require('electron').remote, 7 | currentWindow = remote.getCurrentWindow(), 8 | dataWindow = remote.getGlobal('windowsData')[currentWindow.id]; 9 | 10 | __electron_html_to.ipc = require('electron').ipcRenderer; 11 | __electron_html_to.sliced = require('sliced'); 12 | __electron_html_to.windowId = currentWindow.id; 13 | 14 | __electron_html_to.ipc.send('log', currentWindow.id, 'loading preload script'); 15 | 16 | window.addEventListener('error', function(pageErr) { 17 | __electron_html_to.ipc.send('page-error', __electron_html_to.windowId, pageErr.message, pageErr.error.stack); 18 | }); 19 | 20 | var defaultLog = console.log, 21 | defaultErrorLog = console.error, 22 | defaultWarnLog = console.warn; 23 | 24 | console.log = function() { 25 | var newArgs = __electron_html_to.sliced(arguments); 26 | 27 | newArgs.unshift('debug'); 28 | newArgs.unshift(__electron_html_to.windowId); 29 | 30 | __electron_html_to.ipc.send('page-log', newArgs); 31 | return defaultLog.apply(this, __electron_html_to.sliced(arguments)); 32 | }; 33 | 34 | console.error = function() { 35 | var newArgs = __electron_html_to.sliced(arguments); 36 | 37 | newArgs.unshift('error'); 38 | newArgs.unshift(__electron_html_to.windowId); 39 | 40 | __electron_html_to.ipc.send('page-log', newArgs); 41 | return defaultErrorLog.apply(this, __electron_html_to.sliced(arguments)); 42 | }; 43 | 44 | console.warn = function() { 45 | var newArgs = __electron_html_to.sliced(arguments); 46 | 47 | newArgs.unshift('warn'); 48 | newArgs.unshift(__electron_html_to.windowId); 49 | 50 | __electron_html_to.ipc.send('page-log', newArgs); 51 | return defaultWarnLog.apply(this, __electron_html_to.sliced(arguments)); 52 | }; 53 | 54 | if (dataWindow.waitForJS) { 55 | if (typeof Object.defineProperty === 'function') { 56 | __electron_html_to.ipc.send('log', __electron_html_to.windowId, 'defining waitForJS callback..'); 57 | 58 | Object.defineProperty(window, dataWindow.waitForJSVarName, { 59 | set: function(val) { 60 | if (!val) { 61 | return; 62 | } 63 | 64 | if (val === true) { 65 | __electron_html_to.ipc.send('log', __electron_html_to.windowId, 'waitForJS callback called..'); 66 | __electron_html_to.ipc.send(__electron_html_to.windowId + ':waitForJS'); 67 | } 68 | } 69 | }); 70 | } 71 | 72 | remote = null; 73 | currentWindow = null; 74 | dataWindow = null; 75 | } else { 76 | remote = null; 77 | currentWindow = null; 78 | dataWindow = null; 79 | } 80 | })(); 81 | -------------------------------------------------------------------------------- /src/scripts/serverScript.js: -------------------------------------------------------------------------------- 1 | 2 | const util = require('util'), 3 | http = require('http'), 4 | // disabling import rule because `electron` is a built-in module 5 | // eslint-disable-next-line import/no-unresolved 6 | electron = require('electron'), 7 | jsonBody = require('body/json'), 8 | sliced = require('sliced'), 9 | getBrowserWindowOpts = require('./getBrowserWindowOpts'), 10 | listenRequestsInPage = require('./listenRequestsInPage'), 11 | conversionScript = require('./conversionScript'), 12 | evaluate = require('./evaluateJS'), 13 | parentChannel = require('../ipc')(process), 14 | app = electron.app, 15 | renderer = electron.ipcMain, 16 | BrowserWindow = electron.BrowserWindow; 17 | 18 | let windows = [], 19 | electronVersion, 20 | log, 21 | PORT, 22 | WORKER_ID, 23 | DEBUG_MODE, 24 | CHROME_COMMAND_LINE_SWITCHES, 25 | ALLOW_LOCAL_FILES_ACCESS, 26 | MAX_LOG_ENTRY_SIZE; 27 | 28 | if (process.versions.electron) { 29 | electronVersion = process.versions.electron; 30 | } else if (process.versions['atom-shell']) { 31 | electronVersion = process.versions['atom-shell']; 32 | } else { 33 | electronVersion = ''; 34 | } 35 | 36 | PORT = process.env.ELECTRON_WORKER_PORT; 37 | WORKER_ID = process.env.ELECTRON_WORKER_ID; 38 | DEBUG_MODE = Boolean(process.env.ELECTRON_HTML_TO_DEBUGGING); 39 | CHROME_COMMAND_LINE_SWITCHES = JSON.parse(process.env.chromeCommandLineSwitches); 40 | ALLOW_LOCAL_FILES_ACCESS = process.env.allowLocalFilesAccess === 'true'; 41 | MAX_LOG_ENTRY_SIZE = parseInt(process.env.maxLogEntrySize, 10); 42 | 43 | if (isNaN(MAX_LOG_ENTRY_SIZE)) { 44 | MAX_LOG_ENTRY_SIZE = 1000; 45 | } 46 | 47 | log = function() { 48 | // eslint-disable-next-line prefer-rest-params 49 | let newArgs = sliced(arguments); 50 | 51 | newArgs.unshift(`[Worker ${WORKER_ID}]`); 52 | 53 | parentChannel.emit.apply(parentChannel, ['log'].concat(newArgs)); 54 | }; 55 | 56 | global.windowsData = {}; 57 | global.windowsLogs = {}; 58 | 59 | Object.keys(CHROME_COMMAND_LINE_SWITCHES).forEach((switchName) => { 60 | let switchValue = CHROME_COMMAND_LINE_SWITCHES[switchName]; 61 | 62 | if (switchValue != null) { 63 | log(`establishing chrome command line switch [${switchName}:${switchValue}]`); 64 | app.commandLine.appendSwitch(switchName, switchValue); 65 | } else { 66 | log(`establishing chrome command line switch [${switchName}]`); 67 | app.commandLine.appendSwitch(switchName); 68 | } 69 | }); 70 | 71 | if (app.dock && typeof app.dock.hide === 'function') { 72 | if (!DEBUG_MODE) { 73 | app.dock.hide(); 74 | } 75 | } 76 | 77 | app.on('window-all-closed', () => { 78 | // by default dont close the app (because the electron server will be running) 79 | // only close when debug mode is on 80 | if (DEBUG_MODE) { 81 | app.quit(); 82 | } 83 | }); 84 | 85 | app.on('ready', () => { 86 | let server; 87 | 88 | log('electron process ready..'); 89 | 90 | renderer.on('page-error', (ev, windowId, errMsg, errStack) => { 91 | // saving errors on page 92 | saveLogsInStore(global.windowsLogs[windowId], 'warn', `error in page: ${errMsg}`); 93 | 94 | saveLogsInStore(global.windowsLogs[windowId], 'warn', `error in page stack: ${errStack}`); 95 | 96 | parentChannel.emit('page-error', windowId, errMsg, errStack); 97 | }); 98 | 99 | renderer.on('page-log', (ev, args) => { 100 | let windowId = args[0], 101 | logLevel = args[1], 102 | logArgs = args.slice(2), 103 | // removing log level argument 104 | newArgs = args.slice(0, 1).concat(logArgs); 105 | 106 | // saving logs 107 | saveLogsInStore(global.windowsLogs[windowId], logLevel, logArgs, true); 108 | 109 | parentChannel.emit.apply(parentChannel, ['page-log'].concat(newArgs)); 110 | }); 111 | 112 | renderer.on('log', function() { 113 | // eslint-disable-next-line prefer-rest-params 114 | let newArgs = sliced(arguments), 115 | windowId = newArgs.splice(0, 2)[1]; 116 | 117 | newArgs.unshift(`[Browser window - ${windowId} log ]:`); 118 | 119 | log.apply(log, newArgs); 120 | }); 121 | 122 | server = http.createServer((req, res) => { 123 | log('new request for electron-server..'); 124 | log('parsing request body..'); 125 | 126 | jsonBody(req, res, (err, settingsData) => { 127 | if (err) { 128 | // eslint-disable-next-line no-param-reassign 129 | res.statusCode = 500; 130 | return res.end(err.message); 131 | } 132 | 133 | log('request body parsed..'); 134 | 135 | try { 136 | createBrowserWindow(res, settingsData); 137 | } catch (uncaughtErr) { 138 | // eslint-disable-next-line no-param-reassign 139 | res.statusCode = 500; 140 | res.end(uncaughtErr.message); 141 | } 142 | }); 143 | }); 144 | 145 | server.on('error', (serverErr) => { 146 | log(`an error in the server has ocurred: ${serverErr.message}`); 147 | app.quit(); 148 | }); 149 | 150 | // we don't bind the server to any specific hostname to allow listening 151 | // in any ip address in local server 152 | server.listen(PORT); 153 | }); 154 | 155 | function createBrowserWindow(res, settingsData) { 156 | let evaluateInWindow, 157 | dataForWindow = {}, 158 | browserWindowOpts, 159 | converterPath, 160 | converter, 161 | currentWindow, 162 | currentWindowId, 163 | extraHeaders = ''; 164 | 165 | function respond(err, data) { 166 | let errMsg = null; 167 | 168 | log('finishing work in browser-window..'); 169 | 170 | if (err) { 171 | errMsg = err.message; 172 | // eslint-disable-next-line no-param-reassign 173 | res.statusCode = 500; 174 | return res.end(errMsg); 175 | } 176 | 177 | if (settingsData.collectLogs) { 178 | // eslint-disable-next-line no-param-reassign 179 | data.logs = global.windowsLogs[currentWindowId]; 180 | } else { 181 | // eslint-disable-next-line no-param-reassign 182 | data.logs = []; 183 | } 184 | 185 | res.setHeader('Content-Type', 'application/json'); 186 | res.end(JSON.stringify(data)); 187 | 188 | if (!currentWindow) { 189 | return; 190 | } 191 | 192 | // in debug mode, don't destroy the browser window 193 | if (!DEBUG_MODE) { 194 | log('destroying browser window..'); 195 | currentWindow.destroy(); 196 | } 197 | } 198 | 199 | converterPath = settingsData.converterPath; 200 | 201 | log(`requiring converter module from ${converterPath}`); 202 | 203 | try { 204 | // eslint-disable-next-line global-require 205 | converter = require(converterPath); 206 | } catch (requireErr) { 207 | return respond(requireErr); 208 | } 209 | 210 | if (settingsData.waitForJS) { 211 | log('waitForJS enabled..'); 212 | 213 | dataForWindow.waitForJS = settingsData.waitForJS; 214 | dataForWindow.waitForJSVarName = settingsData.waitForJSVarName; 215 | } 216 | 217 | // get browser window options with defaults 218 | browserWindowOpts = getBrowserWindowOpts(settingsData.browserWindow); 219 | 220 | log('creating new browser window with options:', browserWindowOpts); 221 | 222 | if (DEBUG_MODE) { 223 | browserWindowOpts.show = true; 224 | } 225 | 226 | if (browserWindowOpts.show) { 227 | log('browser window visibility activated'); 228 | } 229 | 230 | currentWindow = new BrowserWindow(browserWindowOpts); 231 | currentWindowId = currentWindow.id; 232 | addWindow(currentWindow); 233 | 234 | evaluateInWindow = evaluate(currentWindow); 235 | global.windowsData[currentWindowId] = dataForWindow; 236 | global.windowsLogs[currentWindowId] = []; 237 | 238 | saveLogsInStore( 239 | global.windowsLogs[currentWindowId], 240 | 'debug', 241 | `Converting using electron-server strategy in electron ${electronVersion}` 242 | ); 243 | 244 | currentWindow.webContents.setAudioMuted(true); 245 | 246 | listenRequestsInPage( 247 | currentWindow, 248 | { 249 | allowLocalFilesAccess: ALLOW_LOCAL_FILES_ACCESS, 250 | pageUrl: settingsData.url 251 | }, 252 | log, 253 | saveLogsInStore(global.windowsLogs[currentWindowId]) 254 | ); 255 | 256 | currentWindow.on('closed', () => { 257 | log('browser-window closed..'); 258 | 259 | delete global.windowsData[currentWindowId]; 260 | delete global.windowsLogs[currentWindowId]; 261 | 262 | removeWindow(currentWindow); 263 | currentWindow = null; 264 | }); 265 | 266 | conversionScript(settingsData, currentWindow, evaluateInWindow, log, converter, respond); 267 | 268 | if (settingsData.userAgent) { 269 | log(`setting up custom user agent: ${settingsData.userAgent}`); 270 | currentWindow.webContents.setUserAgent(settingsData.userAgent); 271 | } 272 | 273 | if (typeof settingsData.extraHeaders === 'object') { 274 | Object.keys(settingsData.extraHeaders).forEach((key) => { 275 | extraHeaders += `${key}: ${settingsData.extraHeaders[key]}\n`; 276 | }); 277 | } 278 | 279 | log(util.format('loading url in browser window: %s, with headers: %s', settingsData.url, extraHeaders)); 280 | 281 | if (extraHeaders) { 282 | currentWindow.loadURL(settingsData.url, { 283 | extraHeaders 284 | }); 285 | } else { 286 | currentWindow.loadURL(settingsData.url); 287 | } 288 | 289 | // useful in windows to prevent the electron process to hang.. 290 | currentWindow.focus(); 291 | } 292 | 293 | function addWindow(browserWindow) { 294 | windows.push(browserWindow); 295 | } 296 | 297 | function removeWindow(browserWindow) { 298 | windows.forEach((win, index) => { 299 | if (win === browserWindow) { 300 | windows.splice(index, 1); 301 | } 302 | }); 303 | } 304 | 305 | function saveLogsInStore(store, level, msg, userLevel = false) { 306 | // eslint-disable-next-line prefer-rest-params 307 | let args = sliced(arguments); 308 | 309 | if (args.length === 1) { 310 | return _saveLogs.bind(undefined, store); 311 | } 312 | 313 | return _saveLogs(store, level, msg, userLevel); 314 | 315 | function _saveLogs(_store, _level, _msg, _userLevel) { 316 | const meta = { 317 | level: _level, 318 | message: trimMessage(_msg), 319 | timestamp: new Date().getTime() 320 | }; 321 | 322 | if (_userLevel) { 323 | meta.userLevel = true; 324 | } 325 | 326 | _store.push(meta); 327 | } 328 | } 329 | 330 | function trimMessage(args) { 331 | let message = args; 332 | 333 | if (Array.isArray(args)) { 334 | message = args.join(' '); 335 | } 336 | 337 | if (message.length > MAX_LOG_ENTRY_SIZE) { 338 | return `${message.substring(0, MAX_LOG_ENTRY_SIZE)}...`; 339 | } 340 | 341 | return message; 342 | } 343 | -------------------------------------------------------------------------------- /src/scripts/standaloneScript.js: -------------------------------------------------------------------------------- 1 | 2 | const util = require('util'), 3 | fs = require('fs'), 4 | // disabling import rule because `electron` is a built-in module 5 | // eslint-disable-next-line import/no-unresolved 6 | electron = require('electron'), 7 | sliced = require('sliced'), 8 | getBrowserWindowOpts = require('./getBrowserWindowOpts'), 9 | listenRequestsInPage = require('./listenRequestsInPage'), 10 | conversionScript = require('./conversionScript'), 11 | evaluate = require('./evaluateJS'), 12 | parentChannel = require('../ipc')(process), 13 | app = electron.app, 14 | renderer = electron.ipcMain, 15 | BrowserWindow = electron.BrowserWindow; 16 | 17 | let mainWindow = null, 18 | mainWindowId, 19 | electronVersion, 20 | settingsFile, 21 | settingsData, 22 | converterPath, 23 | converter, 24 | maxLogEntrySize, 25 | log, 26 | windowLogs = [], 27 | WORKER_ID, 28 | DEBUG_MODE; 29 | 30 | settingsFile = process.env.ELECTRON_HTML_TO_SETTINGS_FILE_PATH; 31 | WORKER_ID = process.env.ELECTRON_WORKER_ID; 32 | DEBUG_MODE = Boolean(process.env.ELECTRON_HTML_TO_DEBUGGING); 33 | 34 | if (process.versions.electron) { 35 | electronVersion = process.versions.electron; 36 | } else if (process.versions['atom-shell']) { 37 | electronVersion = process.versions['atom-shell']; 38 | } else { 39 | electronVersion = ''; 40 | } 41 | 42 | log = function() { 43 | // eslint-disable-next-line prefer-rest-params 44 | let newArgs = sliced(arguments); 45 | 46 | newArgs.unshift(`[Worker ${WORKER_ID}]`); 47 | 48 | parentChannel.emit.apply(parentChannel, ['log'].concat(newArgs)); 49 | }; 50 | 51 | global.windowsData = {}; 52 | 53 | log(`reading settings file from ${settingsFile}`); 54 | settingsData = fs.readFileSync(settingsFile).toString(); 55 | 56 | settingsData = JSON.parse(settingsData); 57 | converterPath = settingsData.converterPath; 58 | maxLogEntrySize = parseInt(settingsData.maxLogEntrySize, 10); 59 | 60 | if (isNaN(maxLogEntrySize)) { 61 | maxLogEntrySize = 1000; 62 | } 63 | 64 | log(`requiring converter module from ${converterPath}`); 65 | converter = require(converterPath); 66 | 67 | Object.keys(settingsData.chromeCommandLineSwitches).forEach((switchName) => { 68 | let switchValue = settingsData.chromeCommandLineSwitches[switchName]; 69 | 70 | if (switchValue != null) { 71 | log(`establishing chrome command line switch [${switchName}:${switchValue}]`); 72 | app.commandLine.appendSwitch(switchName, switchValue); 73 | } else { 74 | log(`establishing chrome command line switch [${switchName}]`); 75 | app.commandLine.appendSwitch(switchName); 76 | } 77 | }); 78 | 79 | app.on('window-all-closed', () => { 80 | log('exiting electron process..'); 81 | app.quit(); 82 | }); 83 | 84 | if (app.dock && typeof app.dock.hide === 'function') { 85 | if (!DEBUG_MODE) { 86 | app.dock.hide(); 87 | } 88 | } 89 | 90 | app.on('ready', () => { 91 | let evaluateInWindow, 92 | dataForWindow = {}, 93 | extraHeaders = '', 94 | browserWindowOpts; 95 | 96 | log('electron process ready..'); 97 | 98 | if (settingsData.waitForJS) { 99 | log('waitForJS enabled..'); 100 | 101 | dataForWindow.waitForJS = settingsData.waitForJS; 102 | dataForWindow.waitForJSVarName = settingsData.waitForJSVarName; 103 | } 104 | 105 | renderer.on('page-error', (ev, windowId, errMsg, errStack) => { 106 | // saving errors on page 107 | saveLogsInStore(windowLogs, 'warn', `error in page: ${errMsg}`); 108 | 109 | saveLogsInStore(windowLogs, 'warn', `error in page stack: ${errStack}`); 110 | 111 | parentChannel.emit('page-error', windowId, errMsg, errStack); 112 | }); 113 | 114 | renderer.on('page-log', (ev, args) => { 115 | let logLevel = args[1], 116 | logArgs = args.slice(2), 117 | // removing log level argument 118 | newArgs = args.slice(0, 1).concat(logArgs); 119 | 120 | // saving logs 121 | saveLogsInStore(windowLogs, logLevel, logArgs, true); 122 | 123 | parentChannel.emit.apply(parentChannel, ['page-log'].concat(newArgs)); 124 | }); 125 | 126 | renderer.on('log', function() { 127 | // eslint-disable-next-line prefer-rest-params 128 | let newArgs = sliced(arguments), 129 | windowId = newArgs.splice(0, 2)[1]; 130 | 131 | newArgs.unshift(`[Browser window - ${windowId} log ]:`); 132 | 133 | log.apply(log, newArgs); 134 | }); 135 | 136 | // get browser window options with defaults 137 | browserWindowOpts = getBrowserWindowOpts(settingsData.browserWindow); 138 | 139 | log('creating new browser window with options:', browserWindowOpts); 140 | 141 | if (DEBUG_MODE) { 142 | browserWindowOpts.show = true; 143 | } 144 | 145 | if (browserWindowOpts.show) { 146 | log('browser window visibility activated'); 147 | } 148 | 149 | mainWindow = new BrowserWindow(browserWindowOpts); 150 | mainWindowId = mainWindow.id; 151 | 152 | evaluateInWindow = evaluate(mainWindow); 153 | global.windowsData[mainWindowId] = dataForWindow; 154 | 155 | saveLogsInStore(windowLogs, 'debug', `Converting using dedicated-process strategy in electron ${electronVersion}`); 156 | 157 | mainWindow.webContents.setAudioMuted(true); 158 | 159 | listenRequestsInPage( 160 | mainWindow, 161 | { 162 | allowLocalFilesAccess: settingsData.allowLocalFilesAccess, 163 | pageUrl: settingsData.url 164 | }, 165 | log, 166 | saveLogsInStore(windowLogs) 167 | ); 168 | 169 | mainWindow.on('closed', () => { 170 | log('browser-window closed..'); 171 | 172 | delete global.windowsData[mainWindowId]; 173 | mainWindow = null; 174 | }); 175 | 176 | conversionScript(settingsData, mainWindow, evaluateInWindow, log, converter, respond); 177 | 178 | if (settingsData.userAgent) { 179 | log(`setting up custom user agent: ${settingsData.userAgent}`); 180 | mainWindow.webContents.setUserAgent(settingsData.userAgent); 181 | } 182 | 183 | if (typeof settingsData.extraHeaders === 'object') { 184 | Object.keys(settingsData.extraHeaders).forEach((key) => { 185 | extraHeaders += `${key}: ${settingsData.extraHeaders[key]}\n`; 186 | }); 187 | } 188 | 189 | log(util.format('loading url in browser window: %s, with headers: %s', settingsData.url, extraHeaders)); 190 | 191 | if (extraHeaders) { 192 | mainWindow.loadURL(settingsData.url, { 193 | extraHeaders 194 | }); 195 | } else { 196 | mainWindow.loadURL(settingsData.url); 197 | } 198 | 199 | // useful in windows to prevent the electron process to hang.. 200 | mainWindow.focus(); 201 | }); 202 | 203 | function respond(err, data) { 204 | let errMsg = null; 205 | 206 | log('finishing work in browser-window..'); 207 | 208 | if (err) { 209 | errMsg = err.message; 210 | } 211 | 212 | if (settingsData.collectLogs) { 213 | // eslint-disable-next-line no-param-reassign 214 | data.logs = windowLogs; 215 | } else { 216 | // eslint-disable-next-line no-param-reassign 217 | data.logs = []; 218 | } 219 | 220 | parentChannel.emit('finish', errMsg, data); 221 | 222 | if (!mainWindow) { 223 | return; 224 | } 225 | 226 | // in debug mode, don't destroy the browser window 227 | if (!DEBUG_MODE) { 228 | log('destroying browser window..'); 229 | mainWindow.destroy(); 230 | } 231 | } 232 | 233 | function saveLogsInStore(store, level, msg, userLevel = false) { 234 | // eslint-disable-next-line prefer-rest-params 235 | let args = sliced(arguments); 236 | 237 | if (args.length === 1) { 238 | return _saveLogs.bind(undefined, store); 239 | } 240 | 241 | return _saveLogs(store, level, msg, userLevel); 242 | 243 | function _saveLogs(_store, _level, _msg, _userLevel) { 244 | const meta = { 245 | level: _level, 246 | message: trimMessage(_msg), 247 | timestamp: new Date().getTime() 248 | }; 249 | 250 | if (_userLevel) { 251 | meta.userLevel = true; 252 | } 253 | 254 | _store.push(meta); 255 | } 256 | } 257 | 258 | function trimMessage(args) { 259 | let message = args; 260 | 261 | if (Array.isArray(args)) { 262 | message = args.join(' '); 263 | } 264 | 265 | if (message.length > maxLogEntrySize) { 266 | return `${message.substring(0, maxLogEntrySize)}...`; 267 | } 268 | 269 | return message; 270 | } 271 | -------------------------------------------------------------------------------- /src/serverIpcStrategy.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import debug from 'debug'; 5 | import electronWorkers from 'electron-workers'; 6 | import ensureStart from './ensureStartWorker'; 7 | import { name as pkgName } from '../package.json'; 8 | 9 | const debugServerStrategy = debug(`${pkgName}:electron-server-strategy`), 10 | debugIpcStrategy = debug(`${pkgName}:electron-ipc-strategy`); 11 | 12 | export default function(mode, options) { 13 | let debugMode = false, 14 | scriptPath, 15 | debugStrategy; 16 | 17 | if (mode === 'server') { 18 | scriptPath = path.join(__dirname, 'scripts', 'serverScript.js'); 19 | debugStrategy = debugServerStrategy; 20 | } else if (mode === 'ipc') { 21 | scriptPath = path.join(__dirname, 'scripts', 'ipcScript.js'); 22 | debugStrategy = debugIpcStrategy; 23 | } else { 24 | // defaults to server script and a no-op function 25 | scriptPath = path.join(__dirname, 'scripts', 'serverScript.js'); 26 | debugStrategy = () => {}; 27 | } 28 | 29 | const workersOptions = { ...options, pathToScript: scriptPath, env: {} }; 30 | 31 | if (mode) { 32 | workersOptions.connectionMode = mode; 33 | } 34 | 35 | if (process.env.ELECTRON_HTML_TO_DEBUGGING !== undefined) { 36 | debugMode = true; 37 | workersOptions.env.ELECTRON_HTML_TO_DEBUGGING = process.env.ELECTRON_HTML_TO_DEBUGGING; 38 | } 39 | 40 | if (process.env.ELECTRON_ENABLE_LOGGING !== undefined) { 41 | workersOptions.env.ELECTRON_ENABLE_LOGGING = process.env.ELECTRON_ENABLE_LOGGING; 42 | } 43 | 44 | if (process.env.IISNODE_VERSION !== undefined) { 45 | workersOptions.env.IISNODE_VERSION = process.env.IISNODE_VERSION; 46 | } 47 | 48 | workersOptions.env.maxLogEntrySize = options.maxLogEntrySize; 49 | workersOptions.env.chromeCommandLineSwitches = JSON.stringify(options.chromeCommandLineSwitches || {}); 50 | workersOptions.env.allowLocalFilesAccess = JSON.stringify(options.allowLocalFilesAccess || false); 51 | 52 | workersOptions.stdio = [null, null, null, 'ipc']; 53 | 54 | if (debugMode || process.env.ELECTRON_ENABLE_LOGGING !== undefined || process.env.ELECTRON_HTML_TO_STDSTREAMS !== undefined) { 55 | workersOptions.stdio = [null, process.stdout, process.stderr, 'ipc']; 56 | } 57 | 58 | workersOptions.killSignal = 'SIGKILL'; 59 | 60 | const workers = electronWorkers(workersOptions); 61 | 62 | function serverIpcStrategyCall(requestOptions, converterPath, id, cb) { 63 | let executeOpts = {}; 64 | 65 | if (debugMode) { 66 | debugStrategy('electron process debugging mode activated'); 67 | } 68 | 69 | if (process.env.IISNODE_VERSION !== undefined) { 70 | debugStrategy('running in IISNODE..'); 71 | } 72 | 73 | debugStrategy('checking if electron workers have started..'); 74 | 75 | ensureStart(debugStrategy, workers, serverIpcStrategyCall, (err) => { 76 | if (err) { 77 | debugStrategy('electron workers could not start..'); 78 | debugStrategy('conversion ended with error..'); 79 | return cb(err); 80 | } 81 | 82 | debugStrategy('processing conversion..'); 83 | 84 | if (requestOptions.timeout != null) { 85 | executeOpts.timeout = requestOptions.timeout; 86 | } 87 | 88 | workers.execute({ ...requestOptions, converterPath }, executeOpts, (executeErr, res) => { 89 | if (executeErr) { 90 | debugStrategy('conversion ended with error..'); 91 | 92 | // if the error is a timeout from electron-workers 93 | if (executeErr.workerTimeout) { 94 | // eslint-disable-next-line no-param-reassign 95 | executeErr.electronTimeout = true; 96 | } 97 | 98 | return cb(executeErr); 99 | } 100 | 101 | let { output, ...restData } = res; 102 | 103 | debugStrategy('conversion ended successfully..'); 104 | 105 | if (Array.isArray(restData.logs)) { 106 | restData.logs.forEach((m) => { 107 | // eslint-disable-next-line no-param-reassign 108 | m.timestamp = new Date(m.timestamp); 109 | }); 110 | } 111 | 112 | // disabling no-undef rule because eslint don't detect object rest spread correctly 113 | /* eslint-disable no-undef */ 114 | cb(null, { 115 | ...restData, 116 | stream: fs.createReadStream(output) 117 | }); 118 | /* eslint-enable no-undef */ 119 | }); 120 | }); 121 | } 122 | 123 | serverIpcStrategyCall.startCb = []; 124 | 125 | serverIpcStrategyCall.kill = () => { 126 | if (!serverIpcStrategyCall.started) { 127 | return; 128 | } 129 | 130 | debugStrategy('killing electron workers..'); 131 | 132 | serverIpcStrategyCall.started = false; 133 | serverIpcStrategyCall.startCb = []; 134 | workers.kill(); 135 | }; 136 | 137 | return serverIpcStrategyCall; 138 | } 139 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true // adds all of the Mocha testing global variables. 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel/register 2 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import should from 'should'; 5 | import convertFactory from '../src/index'; 6 | 7 | const tmpDir = path.join(__dirname, 'temp'); 8 | 9 | function createConversion(strategy) { 10 | return convertFactory({ 11 | converterPath: convertFactory.converters.PDF, 12 | timeout: 10000, 13 | tmpDir, 14 | portLeftBoundary: 10000, 15 | portRightBoundary: 15000, 16 | strategy 17 | }); 18 | } 19 | 20 | function rmDir(dirPath) { 21 | let files; 22 | 23 | if (!fs.existsSync(dirPath)) { 24 | fs.mkdirSync(dirPath); 25 | } 26 | 27 | try { 28 | files = fs.readdirSync(dirPath); 29 | } catch (err) { 30 | return; 31 | } 32 | 33 | if (files.length > 0) { 34 | for (let ix = 0; ix < files.length; ix++) { 35 | let filePath = `${dirPath}/${files[ix]}`; 36 | 37 | if (fs.statSync(filePath).isFile()) { 38 | fs.unlinkSync(filePath); 39 | } 40 | } 41 | } 42 | } 43 | 44 | /* eslint-disable padded-blocks */ 45 | /* eslint-disable prefer-arrow-callback */ 46 | describe('electron html to pdf', () => { 47 | describe('dedicated-process', () => { 48 | common('dedicated-process'); 49 | }); 50 | 51 | describe('electron-server', () => { 52 | common('electron-server'); 53 | }); 54 | 55 | describe('electron-ipc', () => { 56 | common('electron-ipc'); 57 | }); 58 | 59 | function common(strategy) { 60 | let conversion = createConversion(strategy); 61 | 62 | after(() => { 63 | rmDir(tmpDir); 64 | }); 65 | 66 | it('should set number of pages correctly', function(done) { 67 | conversion('

aa

bb

', (err, res) => { 68 | if (err) { 69 | return done(err); 70 | } 71 | 72 | res.numberOfPages.should.be.eql(2); 73 | res.stream.close(); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('should create a pdf file', function(done) { 79 | conversion('

foo

', (err, res) => { 80 | if (err) { 81 | return done(err); 82 | } 83 | 84 | should(res.numberOfPages).be.eql(1); 85 | should(res.stream).have.property('readable'); 86 | res.stream.close(); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should create a pdf file with header'); 92 | 93 | it('should create a pdf file with footer'); 94 | 95 | it('should create a pdf file with header and footer'); 96 | 97 | it('should create a pdf file ignoring ssl errors', function(done) { 98 | conversion({ 99 | url: 'https://sygris.com' 100 | }, (err, res) => { 101 | if (err) { 102 | return done(err); 103 | } 104 | 105 | should(res.numberOfPages).be.eql(1); 106 | should(res.stream).have.property('readable'); 107 | res.stream.close(); 108 | done(); 109 | }); 110 | }); 111 | 112 | it('should wait for page js execution', function(done) { 113 | conversion({ 114 | html: '

aa

', 115 | waitForJS: true 116 | }, function(err, res) { 117 | if (err) { 118 | return done(err); 119 | } 120 | 121 | should(res.numberOfPages).be.eql(1); 122 | should(res.stream).have.property('readable'); 123 | res.stream.close(); 124 | done(); 125 | }); 126 | }); 127 | 128 | it('should wait for page async js execution', function(done) { 129 | conversion({ 130 | html: '

aa

', 131 | waitForJS: true 132 | }, function(err, res) { 133 | if (err) { 134 | return done(err); 135 | } 136 | 137 | should(res.numberOfPages).be.eql(1); 138 | should(res.stream).have.property('readable'); 139 | res.stream.close(); 140 | done(); 141 | }); 142 | }); 143 | 144 | it('should allow define a custom var name for page js execution', function(done) { 145 | conversion({ 146 | html: '

aa

', 147 | waitForJS: true, 148 | waitForJSVarName: 'ready' 149 | }, function(err, res) { 150 | if (err) { 151 | return done(err); 152 | } 153 | 154 | should(res.numberOfPages).be.eql(1); 155 | should(res.stream).have.property('readable'); 156 | res.stream.close(); 157 | done(); 158 | }); 159 | }); 160 | 161 | it('should throw timeout when waiting for page js execution', function(done) { 162 | conversion({ 163 | html: '

aa

', 164 | timeout: 500, 165 | waitForJS: true 166 | }, function(err) { 167 | if (!err) { 168 | return done(new Error('the conversion doesn\'t throw error')); 169 | } 170 | 171 | if (err.electronTimeout !== undefined) { 172 | should(err.electronTimeout).be.eql(true); 173 | done(); 174 | } else { 175 | done(err); 176 | } 177 | }); 178 | }); 179 | 180 | it('should work with javascript disabled in web page', function(done) { 181 | conversion({ 182 | html: '

foo

', 183 | browserWindow: { 184 | webPreferences: { 185 | javascript: false 186 | } 187 | } 188 | }, function(err, res) { 189 | if (err) { 190 | return done(err); 191 | } 192 | 193 | should(res.numberOfPages).be.eql(1); 194 | should(res.stream).have.property('readable'); 195 | res.stream.close(); 196 | done(); 197 | }); 198 | }); 199 | 200 | it('should collect logs in page', function(done) { 201 | conversion({ 202 | html: ` 203 |

aa

204 | 209 | ` 210 | }, function(err, res) { 211 | if (err) { 212 | return done(err); 213 | } 214 | 215 | should(res.logs.length).be.aboveOrEqual(3); 216 | 217 | should(res.numberOfPages).be.eql(1); 218 | should(res.stream).have.property('readable'); 219 | res.stream.close(); 220 | done(); 221 | }); 222 | }); 223 | } 224 | 225 | describe('failing to start workers', () => { 226 | it('should not accumulate error callbacks', (done) => { 227 | let conversion = convertFactory({ 228 | converterPath: convertFactory.converters.PDF, 229 | pathToElectron: 'invalid', 230 | timeout: 10000, 231 | tmpDir, 232 | portLeftBoundary: 10000, 233 | portRightBoundary: 15000, 234 | strategy: 'electron-ipc' 235 | }); 236 | 237 | let cbCounter = 0; 238 | 239 | conversion({ 240 | html: 'test' 241 | }, function() { 242 | cbCounter++; 243 | 244 | conversion({ 245 | html: 'test' 246 | }, function() { 247 | cbCounter++; 248 | setTimeout(function() { 249 | cbCounter.should.be.eql(2); 250 | done(); 251 | }, 0); 252 | }); 253 | }); 254 | }); 255 | }); 256 | }); 257 | --------------------------------------------------------------------------------