├── .jshintrc ├── .npm └── package │ ├── .gitignore │ ├── README │ └── npm-shrinkwrap.json ├── .versions ├── LICENSE ├── README.md ├── app ├── .jshintrc ├── autoUpdater.js ├── main.js ├── menu.js ├── package.json ├── preload.js └── proxyWindowEvents.js ├── client ├── .jshintrc └── index.js ├── docs └── overview.png ├── package.js ├── server ├── .jshintrc ├── createBinaries.js ├── downloadUrls.js ├── index.js ├── launchApp.js ├── serve.js ├── serveDownloadUrl.js └── serveUpdateFeed.js └── tests └── server ├── .jshintrc └── downloadUrlsTest.js /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // If you are using SublimeLinter, after modifying this config file, be sure to close and reopen 3 | // the file(s) you were editing to see the changes take effect. 4 | 5 | // Suppress warnings about using [] notation when it can be expressed in dot notation, 6 | // so that we can use [] notation to highlight when objects are used as maps. 7 | "sub": true, 8 | 9 | // Prohibit the use of undeclared variables. Define globals and/or use JSHint pre-defined 10 | // environments (http://jshint.com/docs/options/#environments) as appropriate. You can set 11 | // environments for specific folders (e.g. client vs. server) by extending this file: 12 | // http://stackoverflow.com/a/25213836/495611. 13 | "undef": true, 14 | 15 | // Warn for unused variables and function parameters. 16 | "unused": true 17 | 18 | // We don't whitelist global variables here because there's nothing that's shared between the 19 | // client, the server, _and_ the app. 20 | } 21 | -------------------------------------------------------------------------------- /.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "electron-packager": { 4 | "version": "https://github.com/mixmaxhq/electron-packager/archive/f511e2680efa39c014d8bedca872168e585f8daf.tar.gz", 5 | "dependencies": { 6 | "asar": { 7 | "version": "0.8.3", 8 | "dependencies": { 9 | "chromium-pickle-js": { 10 | "version": "0.1.0" 11 | }, 12 | "commander": { 13 | "version": "2.3.0" 14 | }, 15 | "cuint": { 16 | "version": "0.1.5" 17 | }, 18 | "minimatch": { 19 | "version": "2.0.4", 20 | "dependencies": { 21 | "brace-expansion": { 22 | "version": "1.1.2", 23 | "dependencies": { 24 | "balanced-match": { 25 | "version": "0.3.0" 26 | }, 27 | "concat-map": { 28 | "version": "0.0.1" 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | "mksnapshot": { 35 | "version": "0.1.0", 36 | "dependencies": { 37 | "decompress-zip": { 38 | "version": "0.1.0", 39 | "dependencies": { 40 | "binary": { 41 | "version": "0.3.0", 42 | "dependencies": { 43 | "chainsaw": { 44 | "version": "0.1.0", 45 | "dependencies": { 46 | "traverse": { 47 | "version": "0.3.9" 48 | } 49 | } 50 | }, 51 | "buffers": { 52 | "version": "0.1.1" 53 | } 54 | } 55 | }, 56 | "graceful-fs": { 57 | "version": "3.0.8" 58 | }, 59 | "mkpath": { 60 | "version": "0.1.0" 61 | }, 62 | "nopt": { 63 | "version": "3.0.6", 64 | "dependencies": { 65 | "abbrev": { 66 | "version": "1.0.7" 67 | } 68 | } 69 | }, 70 | "q": { 71 | "version": "1.4.1" 72 | }, 73 | "readable-stream": { 74 | "version": "1.1.13", 75 | "dependencies": { 76 | "core-util-is": { 77 | "version": "1.0.2" 78 | }, 79 | "isarray": { 80 | "version": "0.0.1" 81 | }, 82 | "string_decoder": { 83 | "version": "0.10.31" 84 | }, 85 | "inherits": { 86 | "version": "2.0.1" 87 | } 88 | } 89 | }, 90 | "touch": { 91 | "version": "0.0.3", 92 | "dependencies": { 93 | "nopt": { 94 | "version": "1.0.10", 95 | "dependencies": { 96 | "abbrev": { 97 | "version": "1.0.7" 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "fs-extra": { 106 | "version": "0.18.2", 107 | "dependencies": { 108 | "graceful-fs": { 109 | "version": "3.0.8" 110 | }, 111 | "jsonfile": { 112 | "version": "2.2.3" 113 | } 114 | } 115 | }, 116 | "request": { 117 | "version": "2.55.0", 118 | "dependencies": { 119 | "bl": { 120 | "version": "0.9.4", 121 | "dependencies": { 122 | "readable-stream": { 123 | "version": "1.0.33", 124 | "dependencies": { 125 | "core-util-is": { 126 | "version": "1.0.2" 127 | }, 128 | "isarray": { 129 | "version": "0.0.1" 130 | }, 131 | "string_decoder": { 132 | "version": "0.10.31" 133 | }, 134 | "inherits": { 135 | "version": "2.0.1" 136 | } 137 | } 138 | } 139 | } 140 | }, 141 | "caseless": { 142 | "version": "0.9.0" 143 | }, 144 | "forever-agent": { 145 | "version": "0.6.1" 146 | }, 147 | "form-data": { 148 | "version": "0.2.0", 149 | "dependencies": { 150 | "async": { 151 | "version": "0.9.2" 152 | } 153 | } 154 | }, 155 | "json-stringify-safe": { 156 | "version": "5.0.1" 157 | }, 158 | "mime-types": { 159 | "version": "2.0.14", 160 | "dependencies": { 161 | "mime-db": { 162 | "version": "1.12.0" 163 | } 164 | } 165 | }, 166 | "node-uuid": { 167 | "version": "1.4.7" 168 | }, 169 | "qs": { 170 | "version": "2.4.2" 171 | }, 172 | "tunnel-agent": { 173 | "version": "0.4.2" 174 | }, 175 | "tough-cookie": { 176 | "version": "2.2.1" 177 | }, 178 | "http-signature": { 179 | "version": "0.10.1", 180 | "dependencies": { 181 | "assert-plus": { 182 | "version": "0.1.5" 183 | }, 184 | "asn1": { 185 | "version": "0.1.11" 186 | }, 187 | "ctype": { 188 | "version": "0.5.3" 189 | } 190 | } 191 | }, 192 | "oauth-sign": { 193 | "version": "0.6.0" 194 | }, 195 | "hawk": { 196 | "version": "2.3.1", 197 | "dependencies": { 198 | "hoek": { 199 | "version": "2.16.3" 200 | }, 201 | "boom": { 202 | "version": "2.10.1" 203 | }, 204 | "cryptiles": { 205 | "version": "2.0.5" 206 | }, 207 | "sntp": { 208 | "version": "1.0.9" 209 | } 210 | } 211 | }, 212 | "aws-sign2": { 213 | "version": "0.5.0" 214 | }, 215 | "stringstream": { 216 | "version": "0.0.5" 217 | }, 218 | "combined-stream": { 219 | "version": "0.0.7", 220 | "dependencies": { 221 | "delayed-stream": { 222 | "version": "0.0.5" 223 | } 224 | } 225 | }, 226 | "isstream": { 227 | "version": "0.1.2" 228 | }, 229 | "har-validator": { 230 | "version": "1.8.0", 231 | "dependencies": { 232 | "bluebird": { 233 | "version": "2.10.2" 234 | }, 235 | "chalk": { 236 | "version": "1.1.1", 237 | "dependencies": { 238 | "ansi-styles": { 239 | "version": "2.1.0" 240 | }, 241 | "escape-string-regexp": { 242 | "version": "1.0.3" 243 | }, 244 | "has-ansi": { 245 | "version": "2.0.0", 246 | "dependencies": { 247 | "ansi-regex": { 248 | "version": "2.0.0" 249 | } 250 | } 251 | }, 252 | "strip-ansi": { 253 | "version": "3.0.0", 254 | "dependencies": { 255 | "ansi-regex": { 256 | "version": "2.0.0" 257 | } 258 | } 259 | }, 260 | "supports-color": { 261 | "version": "2.0.0" 262 | } 263 | } 264 | }, 265 | "commander": { 266 | "version": "2.9.0", 267 | "dependencies": { 268 | "graceful-readlink": { 269 | "version": "1.0.1" 270 | } 271 | } 272 | }, 273 | "is-my-json-valid": { 274 | "version": "2.12.3", 275 | "dependencies": { 276 | "generate-function": { 277 | "version": "2.0.0" 278 | }, 279 | "generate-object-property": { 280 | "version": "1.2.0", 281 | "dependencies": { 282 | "is-property": { 283 | "version": "1.0.2" 284 | } 285 | } 286 | }, 287 | "jsonpointer": { 288 | "version": "2.0.0" 289 | }, 290 | "xtend": { 291 | "version": "4.0.1" 292 | } 293 | } 294 | } 295 | } 296 | } 297 | } 298 | } 299 | } 300 | }, 301 | "glob": { 302 | "version": "5.0.15", 303 | "dependencies": { 304 | "inflight": { 305 | "version": "1.0.4", 306 | "dependencies": { 307 | "wrappy": { 308 | "version": "1.0.1" 309 | } 310 | } 311 | }, 312 | "inherits": { 313 | "version": "2.0.1" 314 | }, 315 | "once": { 316 | "version": "1.3.3", 317 | "dependencies": { 318 | "wrappy": { 319 | "version": "1.0.1" 320 | } 321 | } 322 | }, 323 | "path-is-absolute": { 324 | "version": "1.0.0" 325 | } 326 | } 327 | } 328 | } 329 | }, 330 | "electron-download": { 331 | "version": "1.4.1", 332 | "dependencies": { 333 | "debug": { 334 | "version": "2.2.0", 335 | "dependencies": { 336 | "ms": { 337 | "version": "0.7.1" 338 | } 339 | } 340 | }, 341 | "home-path": { 342 | "version": "1.0.1" 343 | }, 344 | "nugget": { 345 | "version": "1.6.0", 346 | "dependencies": { 347 | "pretty-bytes": { 348 | "version": "1.0.4", 349 | "dependencies": { 350 | "get-stdin": { 351 | "version": "4.0.1" 352 | }, 353 | "meow": { 354 | "version": "3.6.0", 355 | "dependencies": { 356 | "camelcase-keys": { 357 | "version": "2.0.0", 358 | "dependencies": { 359 | "camelcase": { 360 | "version": "2.0.1" 361 | }, 362 | "map-obj": { 363 | "version": "1.0.1" 364 | } 365 | } 366 | }, 367 | "loud-rejection": { 368 | "version": "1.2.0", 369 | "dependencies": { 370 | "signal-exit": { 371 | "version": "2.1.2" 372 | } 373 | } 374 | }, 375 | "normalize-package-data": { 376 | "version": "2.3.5", 377 | "dependencies": { 378 | "hosted-git-info": { 379 | "version": "2.1.4" 380 | }, 381 | "is-builtin-module": { 382 | "version": "1.0.0", 383 | "dependencies": { 384 | "builtin-modules": { 385 | "version": "1.1.0" 386 | } 387 | } 388 | }, 389 | "validate-npm-package-license": { 390 | "version": "3.0.1", 391 | "dependencies": { 392 | "spdx-correct": { 393 | "version": "1.0.2", 394 | "dependencies": { 395 | "spdx-license-ids": { 396 | "version": "1.1.0" 397 | } 398 | } 399 | }, 400 | "spdx-expression-parse": { 401 | "version": "1.0.2", 402 | "dependencies": { 403 | "spdx-exceptions": { 404 | "version": "1.0.4" 405 | }, 406 | "spdx-license-ids": { 407 | "version": "1.1.0" 408 | } 409 | } 410 | } 411 | } 412 | } 413 | } 414 | }, 415 | "object-assign": { 416 | "version": "4.0.1" 417 | }, 418 | "read-pkg-up": { 419 | "version": "1.0.1", 420 | "dependencies": { 421 | "find-up": { 422 | "version": "1.1.0", 423 | "dependencies": { 424 | "path-exists": { 425 | "version": "2.1.0" 426 | }, 427 | "pinkie-promise": { 428 | "version": "2.0.0", 429 | "dependencies": { 430 | "pinkie": { 431 | "version": "2.0.1" 432 | } 433 | } 434 | } 435 | } 436 | }, 437 | "read-pkg": { 438 | "version": "1.1.0", 439 | "dependencies": { 440 | "load-json-file": { 441 | "version": "1.1.0", 442 | "dependencies": { 443 | "graceful-fs": { 444 | "version": "4.1.2" 445 | }, 446 | "parse-json": { 447 | "version": "2.2.0", 448 | "dependencies": { 449 | "error-ex": { 450 | "version": "1.3.0", 451 | "dependencies": { 452 | "is-arrayish": { 453 | "version": "0.2.1" 454 | } 455 | } 456 | } 457 | } 458 | }, 459 | "pify": { 460 | "version": "2.3.0" 461 | }, 462 | "pinkie-promise": { 463 | "version": "2.0.0", 464 | "dependencies": { 465 | "pinkie": { 466 | "version": "2.0.1" 467 | } 468 | } 469 | }, 470 | "strip-bom": { 471 | "version": "2.0.0", 472 | "dependencies": { 473 | "is-utf8": { 474 | "version": "0.2.0" 475 | } 476 | } 477 | } 478 | } 479 | }, 480 | "path-type": { 481 | "version": "1.1.0", 482 | "dependencies": { 483 | "graceful-fs": { 484 | "version": "4.1.2" 485 | }, 486 | "pify": { 487 | "version": "2.3.0" 488 | }, 489 | "pinkie-promise": { 490 | "version": "2.0.0", 491 | "dependencies": { 492 | "pinkie": { 493 | "version": "2.0.1" 494 | } 495 | } 496 | } 497 | } 498 | } 499 | } 500 | } 501 | } 502 | }, 503 | "redent": { 504 | "version": "1.0.0", 505 | "dependencies": { 506 | "indent-string": { 507 | "version": "2.1.0", 508 | "dependencies": { 509 | "repeating": { 510 | "version": "2.0.0", 511 | "dependencies": { 512 | "is-finite": { 513 | "version": "1.0.1", 514 | "dependencies": { 515 | "number-is-nan": { 516 | "version": "1.0.0" 517 | } 518 | } 519 | } 520 | } 521 | } 522 | } 523 | }, 524 | "strip-indent": { 525 | "version": "1.0.1" 526 | } 527 | } 528 | }, 529 | "trim-newlines": { 530 | "version": "1.0.0" 531 | } 532 | } 533 | } 534 | } 535 | }, 536 | "progress-stream": { 537 | "version": "1.2.0", 538 | "dependencies": { 539 | "through2": { 540 | "version": "0.2.3", 541 | "dependencies": { 542 | "readable-stream": { 543 | "version": "1.1.13", 544 | "dependencies": { 545 | "core-util-is": { 546 | "version": "1.0.2" 547 | }, 548 | "isarray": { 549 | "version": "0.0.1" 550 | }, 551 | "string_decoder": { 552 | "version": "0.10.31" 553 | }, 554 | "inherits": { 555 | "version": "2.0.1" 556 | } 557 | } 558 | }, 559 | "xtend": { 560 | "version": "2.1.2", 561 | "dependencies": { 562 | "object-keys": { 563 | "version": "0.4.0" 564 | } 565 | } 566 | } 567 | } 568 | }, 569 | "speedometer": { 570 | "version": "0.1.4" 571 | } 572 | } 573 | }, 574 | "request": { 575 | "version": "2.67.0", 576 | "dependencies": { 577 | "bl": { 578 | "version": "1.0.0", 579 | "dependencies": { 580 | "readable-stream": { 581 | "version": "2.0.4", 582 | "dependencies": { 583 | "core-util-is": { 584 | "version": "1.0.2" 585 | }, 586 | "inherits": { 587 | "version": "2.0.1" 588 | }, 589 | "isarray": { 590 | "version": "0.0.1" 591 | }, 592 | "process-nextick-args": { 593 | "version": "1.0.6" 594 | }, 595 | "string_decoder": { 596 | "version": "0.10.31" 597 | }, 598 | "util-deprecate": { 599 | "version": "1.0.2" 600 | } 601 | } 602 | } 603 | } 604 | }, 605 | "caseless": { 606 | "version": "0.11.0" 607 | }, 608 | "extend": { 609 | "version": "3.0.0" 610 | }, 611 | "forever-agent": { 612 | "version": "0.6.1" 613 | }, 614 | "form-data": { 615 | "version": "1.0.0-rc3", 616 | "dependencies": { 617 | "async": { 618 | "version": "1.5.0" 619 | } 620 | } 621 | }, 622 | "json-stringify-safe": { 623 | "version": "5.0.1" 624 | }, 625 | "mime-types": { 626 | "version": "2.1.8", 627 | "dependencies": { 628 | "mime-db": { 629 | "version": "1.20.0" 630 | } 631 | } 632 | }, 633 | "node-uuid": { 634 | "version": "1.4.7" 635 | }, 636 | "qs": { 637 | "version": "5.2.0" 638 | }, 639 | "tunnel-agent": { 640 | "version": "0.4.2" 641 | }, 642 | "tough-cookie": { 643 | "version": "2.2.1" 644 | }, 645 | "http-signature": { 646 | "version": "1.1.0", 647 | "dependencies": { 648 | "assert-plus": { 649 | "version": "0.1.5" 650 | }, 651 | "jsprim": { 652 | "version": "1.2.2", 653 | "dependencies": { 654 | "extsprintf": { 655 | "version": "1.0.2" 656 | }, 657 | "json-schema": { 658 | "version": "0.2.2" 659 | }, 660 | "verror": { 661 | "version": "1.3.6" 662 | } 663 | } 664 | }, 665 | "sshpk": { 666 | "version": "1.7.1", 667 | "dependencies": { 668 | "asn1": { 669 | "version": "0.2.3" 670 | }, 671 | "assert-plus": { 672 | "version": "0.2.0" 673 | }, 674 | "dashdash": { 675 | "version": "1.10.1", 676 | "dependencies": { 677 | "assert-plus": { 678 | "version": "0.1.5" 679 | } 680 | } 681 | }, 682 | "jsbn": { 683 | "version": "0.1.0" 684 | }, 685 | "tweetnacl": { 686 | "version": "0.13.2" 687 | }, 688 | "jodid25519": { 689 | "version": "1.0.2" 690 | }, 691 | "ecc-jsbn": { 692 | "version": "0.1.1" 693 | } 694 | } 695 | } 696 | } 697 | }, 698 | "oauth-sign": { 699 | "version": "0.8.0" 700 | }, 701 | "hawk": { 702 | "version": "3.1.2", 703 | "dependencies": { 704 | "hoek": { 705 | "version": "2.16.3" 706 | }, 707 | "boom": { 708 | "version": "2.10.1" 709 | }, 710 | "cryptiles": { 711 | "version": "2.0.5" 712 | }, 713 | "sntp": { 714 | "version": "1.0.9" 715 | } 716 | } 717 | }, 718 | "aws-sign2": { 719 | "version": "0.6.0" 720 | }, 721 | "stringstream": { 722 | "version": "0.0.5" 723 | }, 724 | "combined-stream": { 725 | "version": "1.0.5", 726 | "dependencies": { 727 | "delayed-stream": { 728 | "version": "1.0.0" 729 | } 730 | } 731 | }, 732 | "isstream": { 733 | "version": "0.1.2" 734 | }, 735 | "is-typedarray": { 736 | "version": "1.0.0" 737 | }, 738 | "har-validator": { 739 | "version": "2.0.3", 740 | "dependencies": { 741 | "chalk": { 742 | "version": "1.1.1", 743 | "dependencies": { 744 | "ansi-styles": { 745 | "version": "2.1.0" 746 | }, 747 | "escape-string-regexp": { 748 | "version": "1.0.3" 749 | }, 750 | "has-ansi": { 751 | "version": "2.0.0", 752 | "dependencies": { 753 | "ansi-regex": { 754 | "version": "2.0.0" 755 | } 756 | } 757 | }, 758 | "strip-ansi": { 759 | "version": "3.0.0", 760 | "dependencies": { 761 | "ansi-regex": { 762 | "version": "2.0.0" 763 | } 764 | } 765 | }, 766 | "supports-color": { 767 | "version": "2.0.0" 768 | } 769 | } 770 | }, 771 | "commander": { 772 | "version": "2.9.0", 773 | "dependencies": { 774 | "graceful-readlink": { 775 | "version": "1.0.1" 776 | } 777 | } 778 | }, 779 | "is-my-json-valid": { 780 | "version": "2.12.3", 781 | "dependencies": { 782 | "generate-function": { 783 | "version": "2.0.0" 784 | }, 785 | "generate-object-property": { 786 | "version": "1.2.0", 787 | "dependencies": { 788 | "is-property": { 789 | "version": "1.0.2" 790 | } 791 | } 792 | }, 793 | "jsonpointer": { 794 | "version": "2.0.0" 795 | }, 796 | "xtend": { 797 | "version": "4.0.1" 798 | } 799 | } 800 | }, 801 | "pinkie-promise": { 802 | "version": "2.0.0", 803 | "dependencies": { 804 | "pinkie": { 805 | "version": "2.0.1" 806 | } 807 | } 808 | } 809 | } 810 | } 811 | } 812 | }, 813 | "single-line-log": { 814 | "version": "0.4.1" 815 | }, 816 | "throttleit": { 817 | "version": "0.0.2" 818 | } 819 | } 820 | }, 821 | "path-exists": { 822 | "version": "1.0.0" 823 | }, 824 | "rc": { 825 | "version": "1.1.5", 826 | "dependencies": { 827 | "deep-extend": { 828 | "version": "0.4.0" 829 | }, 830 | "ini": { 831 | "version": "1.3.4" 832 | }, 833 | "strip-json-comments": { 834 | "version": "1.0.4" 835 | } 836 | } 837 | } 838 | } 839 | }, 840 | "extract-zip": { 841 | "version": "1.3.0", 842 | "dependencies": { 843 | "async": { 844 | "version": "1.5.0" 845 | }, 846 | "concat-stream": { 847 | "version": "1.5.0", 848 | "dependencies": { 849 | "inherits": { 850 | "version": "2.0.1" 851 | }, 852 | "typedarray": { 853 | "version": "0.0.6" 854 | }, 855 | "readable-stream": { 856 | "version": "2.0.4", 857 | "dependencies": { 858 | "core-util-is": { 859 | "version": "1.0.2" 860 | }, 861 | "isarray": { 862 | "version": "0.0.1" 863 | }, 864 | "process-nextick-args": { 865 | "version": "1.0.6" 866 | }, 867 | "string_decoder": { 868 | "version": "0.10.31" 869 | }, 870 | "util-deprecate": { 871 | "version": "1.0.2" 872 | } 873 | } 874 | } 875 | } 876 | }, 877 | "debug": { 878 | "version": "0.7.4" 879 | }, 880 | "mkdirp": { 881 | "version": "0.5.0", 882 | "dependencies": { 883 | "minimist": { 884 | "version": "0.0.8" 885 | } 886 | } 887 | }, 888 | "yauzl": { 889 | "version": "2.3.1", 890 | "dependencies": { 891 | "fd-slicer": { 892 | "version": "1.0.1" 893 | }, 894 | "pend": { 895 | "version": "1.2.0" 896 | } 897 | } 898 | } 899 | } 900 | }, 901 | "minimist": { 902 | "version": "1.2.0" 903 | }, 904 | "mv": { 905 | "version": "2.1.1" 906 | }, 907 | "plist": { 908 | "version": "1.2.0", 909 | "dependencies": { 910 | "base64-js": { 911 | "version": "0.0.8" 912 | }, 913 | "xmlbuilder": { 914 | "version": "4.0.0", 915 | "dependencies": { 916 | "lodash": { 917 | "version": "3.10.1" 918 | } 919 | } 920 | }, 921 | "xmldom": { 922 | "version": "0.1.19" 923 | }, 924 | "util-deprecate": { 925 | "version": "1.0.2" 926 | } 927 | } 928 | }, 929 | "rcedit": { 930 | "version": "0.3.0" 931 | }, 932 | "run-series": { 933 | "version": "1.1.4" 934 | } 935 | } 936 | }, 937 | "electron-rebuild": { 938 | "version": "1.0.1", 939 | "dependencies": { 940 | "babel-runtime": { 941 | "version": "5.8.35", 942 | "dependencies": { 943 | "core-js": { 944 | "version": "1.2.6" 945 | } 946 | } 947 | }, 948 | "lodash": { 949 | "version": "3.10.1" 950 | }, 951 | "npm": { 952 | "version": "2.14.18", 953 | "dependencies": { 954 | "abbrev": { 955 | "version": "1.0.7" 956 | }, 957 | "ansi": { 958 | "version": "0.3.1" 959 | }, 960 | "ansicolors": { 961 | "version": "0.3.2" 962 | }, 963 | "ansistyles": { 964 | "version": "0.1.3" 965 | }, 966 | "archy": { 967 | "version": "1.0.0" 968 | }, 969 | "async-some": { 970 | "version": "1.0.2" 971 | }, 972 | "block-stream": { 973 | "version": "0.0.8" 974 | }, 975 | "char-spinner": { 976 | "version": "1.0.1" 977 | }, 978 | "chmodr": { 979 | "version": "1.0.2" 980 | }, 981 | "chownr": { 982 | "version": "1.0.1" 983 | }, 984 | "cmd-shim": { 985 | "version": "2.0.1", 986 | "dependencies": { 987 | "graceful-fs": { 988 | "version": "3.0.8" 989 | } 990 | } 991 | }, 992 | "columnify": { 993 | "version": "1.5.4", 994 | "dependencies": { 995 | "wcwidth": { 996 | "version": "1.0.0", 997 | "dependencies": { 998 | "defaults": { 999 | "version": "1.0.3", 1000 | "dependencies": { 1001 | "clone": { 1002 | "version": "1.0.2" 1003 | } 1004 | } 1005 | } 1006 | } 1007 | } 1008 | } 1009 | }, 1010 | "config-chain": { 1011 | "version": "1.1.10", 1012 | "dependencies": { 1013 | "proto-list": { 1014 | "version": "1.2.4" 1015 | } 1016 | } 1017 | }, 1018 | "dezalgo": { 1019 | "version": "1.0.3", 1020 | "dependencies": { 1021 | "asap": { 1022 | "version": "2.0.3" 1023 | } 1024 | } 1025 | }, 1026 | "editor": { 1027 | "version": "1.0.0" 1028 | }, 1029 | "fs-vacuum": { 1030 | "version": "1.2.7" 1031 | }, 1032 | "fs-write-stream-atomic": { 1033 | "version": "1.0.8", 1034 | "dependencies": { 1035 | "iferr": { 1036 | "version": "0.1.5" 1037 | } 1038 | } 1039 | }, 1040 | "fstream": { 1041 | "version": "1.0.8" 1042 | }, 1043 | "fstream-npm": { 1044 | "version": "1.0.7", 1045 | "dependencies": { 1046 | "fstream-ignore": { 1047 | "version": "1.0.3" 1048 | } 1049 | } 1050 | }, 1051 | "github-url-from-git": { 1052 | "version": "1.4.0" 1053 | }, 1054 | "github-url-from-username-repo": { 1055 | "version": "1.0.2" 1056 | }, 1057 | "glob": { 1058 | "version": "5.0.15", 1059 | "dependencies": { 1060 | "path-is-absolute": { 1061 | "version": "1.0.0" 1062 | } 1063 | } 1064 | }, 1065 | "graceful-fs": { 1066 | "version": "4.1.3" 1067 | }, 1068 | "hosted-git-info": { 1069 | "version": "2.1.4" 1070 | }, 1071 | "inflight": { 1072 | "version": "1.0.4" 1073 | }, 1074 | "inherits": { 1075 | "version": "2.0.1" 1076 | }, 1077 | "ini": { 1078 | "version": "1.3.4" 1079 | }, 1080 | "init-package-json": { 1081 | "version": "1.9.3", 1082 | "dependencies": { 1083 | "glob": { 1084 | "version": "6.0.4", 1085 | "dependencies": { 1086 | "path-is-absolute": { 1087 | "version": "1.0.0" 1088 | } 1089 | } 1090 | }, 1091 | "promzard": { 1092 | "version": "0.3.0" 1093 | } 1094 | } 1095 | }, 1096 | "lockfile": { 1097 | "version": "1.0.1" 1098 | }, 1099 | "lru-cache": { 1100 | "version": "3.2.0", 1101 | "dependencies": { 1102 | "pseudomap": { 1103 | "version": "1.0.1" 1104 | } 1105 | } 1106 | }, 1107 | "minimatch": { 1108 | "version": "3.0.0", 1109 | "dependencies": { 1110 | "brace-expansion": { 1111 | "version": "1.1.1", 1112 | "dependencies": { 1113 | "balanced-match": { 1114 | "version": "0.2.1" 1115 | }, 1116 | "concat-map": { 1117 | "version": "0.0.1" 1118 | } 1119 | } 1120 | } 1121 | } 1122 | }, 1123 | "mkdirp": { 1124 | "version": "0.5.1", 1125 | "dependencies": { 1126 | "minimist": { 1127 | "version": "0.0.8" 1128 | } 1129 | } 1130 | }, 1131 | "node-gyp": { 1132 | "version": "3.2.1", 1133 | "dependencies": { 1134 | "glob": { 1135 | "version": "4.5.3", 1136 | "dependencies": { 1137 | "minimatch": { 1138 | "version": "2.0.10", 1139 | "dependencies": { 1140 | "brace-expansion": { 1141 | "version": "1.1.2", 1142 | "dependencies": { 1143 | "balanced-match": { 1144 | "version": "0.3.0" 1145 | }, 1146 | "concat-map": { 1147 | "version": "0.0.1" 1148 | } 1149 | } 1150 | } 1151 | } 1152 | } 1153 | } 1154 | }, 1155 | "minimatch": { 1156 | "version": "1.0.0", 1157 | "dependencies": { 1158 | "lru-cache": { 1159 | "version": "2.7.3" 1160 | }, 1161 | "sigmund": { 1162 | "version": "1.0.1" 1163 | } 1164 | } 1165 | }, 1166 | "npmlog": { 1167 | "version": "1.2.1", 1168 | "dependencies": { 1169 | "are-we-there-yet": { 1170 | "version": "1.0.5", 1171 | "dependencies": { 1172 | "delegates": { 1173 | "version": "0.1.0" 1174 | } 1175 | } 1176 | }, 1177 | "gauge": { 1178 | "version": "1.2.2", 1179 | "dependencies": { 1180 | "has-unicode": { 1181 | "version": "1.0.1" 1182 | }, 1183 | "lodash.pad": { 1184 | "version": "3.1.1", 1185 | "dependencies": { 1186 | "lodash._basetostring": { 1187 | "version": "3.0.1" 1188 | }, 1189 | "lodash._createpadding": { 1190 | "version": "3.6.1", 1191 | "dependencies": { 1192 | "lodash.repeat": { 1193 | "version": "3.0.1" 1194 | } 1195 | } 1196 | } 1197 | } 1198 | }, 1199 | "lodash.padleft": { 1200 | "version": "3.1.1", 1201 | "dependencies": { 1202 | "lodash._basetostring": { 1203 | "version": "3.0.1" 1204 | }, 1205 | "lodash._createpadding": { 1206 | "version": "3.6.1", 1207 | "dependencies": { 1208 | "lodash.repeat": { 1209 | "version": "3.0.1" 1210 | } 1211 | } 1212 | } 1213 | } 1214 | }, 1215 | "lodash.padright": { 1216 | "version": "3.1.1", 1217 | "dependencies": { 1218 | "lodash._basetostring": { 1219 | "version": "3.0.1" 1220 | }, 1221 | "lodash._createpadding": { 1222 | "version": "3.6.1", 1223 | "dependencies": { 1224 | "lodash.repeat": { 1225 | "version": "3.0.1" 1226 | } 1227 | } 1228 | } 1229 | } 1230 | } 1231 | } 1232 | } 1233 | } 1234 | }, 1235 | "path-array": { 1236 | "version": "1.0.0", 1237 | "dependencies": { 1238 | "array-index": { 1239 | "version": "0.1.1", 1240 | "dependencies": { 1241 | "debug": { 1242 | "version": "2.2.0", 1243 | "dependencies": { 1244 | "ms": { 1245 | "version": "0.7.1" 1246 | } 1247 | } 1248 | } 1249 | } 1250 | } 1251 | } 1252 | } 1253 | } 1254 | }, 1255 | "nopt": { 1256 | "version": "3.0.6" 1257 | }, 1258 | "normalize-git-url": { 1259 | "version": "3.0.1" 1260 | }, 1261 | "normalize-package-data": { 1262 | "version": "2.3.5", 1263 | "dependencies": { 1264 | "is-builtin-module": { 1265 | "version": "1.0.0", 1266 | "dependencies": { 1267 | "builtin-modules": { 1268 | "version": "1.1.0" 1269 | } 1270 | } 1271 | } 1272 | } 1273 | }, 1274 | "npm-cache-filename": { 1275 | "version": "1.0.2" 1276 | }, 1277 | "npm-install-checks": { 1278 | "version": "1.0.6", 1279 | "dependencies": { 1280 | "npmlog": { 1281 | "version": "1.2.1", 1282 | "dependencies": { 1283 | "are-we-there-yet": { 1284 | "version": "1.0.4", 1285 | "dependencies": { 1286 | "delegates": { 1287 | "version": "0.1.0" 1288 | } 1289 | } 1290 | }, 1291 | "gauge": { 1292 | "version": "1.2.2", 1293 | "dependencies": { 1294 | "has-unicode": { 1295 | "version": "1.0.1" 1296 | }, 1297 | "lodash.pad": { 1298 | "version": "3.1.1", 1299 | "dependencies": { 1300 | "lodash._basetostring": { 1301 | "version": "3.0.1" 1302 | }, 1303 | "lodash._createpadding": { 1304 | "version": "3.6.1", 1305 | "dependencies": { 1306 | "lodash.repeat": { 1307 | "version": "3.0.1" 1308 | } 1309 | } 1310 | } 1311 | } 1312 | }, 1313 | "lodash.padleft": { 1314 | "version": "3.1.1", 1315 | "dependencies": { 1316 | "lodash._basetostring": { 1317 | "version": "3.0.1" 1318 | }, 1319 | "lodash._createpadding": { 1320 | "version": "3.6.1", 1321 | "dependencies": { 1322 | "lodash.repeat": { 1323 | "version": "3.0.1" 1324 | } 1325 | } 1326 | } 1327 | } 1328 | }, 1329 | "lodash.padright": { 1330 | "version": "3.1.1", 1331 | "dependencies": { 1332 | "lodash._basetostring": { 1333 | "version": "3.0.1" 1334 | }, 1335 | "lodash._createpadding": { 1336 | "version": "3.6.1", 1337 | "dependencies": { 1338 | "lodash.repeat": { 1339 | "version": "3.0.1" 1340 | } 1341 | } 1342 | } 1343 | } 1344 | } 1345 | } 1346 | } 1347 | } 1348 | } 1349 | } 1350 | }, 1351 | "npm-package-arg": { 1352 | "version": "4.1.0" 1353 | }, 1354 | "npm-registry-client": { 1355 | "version": "7.0.9", 1356 | "dependencies": { 1357 | "concat-stream": { 1358 | "version": "1.5.1", 1359 | "dependencies": { 1360 | "typedarray": { 1361 | "version": "0.0.6" 1362 | }, 1363 | "readable-stream": { 1364 | "version": "2.0.4", 1365 | "dependencies": { 1366 | "core-util-is": { 1367 | "version": "1.0.2" 1368 | }, 1369 | "isarray": { 1370 | "version": "0.0.1" 1371 | }, 1372 | "process-nextick-args": { 1373 | "version": "1.0.6" 1374 | }, 1375 | "string_decoder": { 1376 | "version": "0.10.31" 1377 | }, 1378 | "util-deprecate": { 1379 | "version": "1.0.2" 1380 | } 1381 | } 1382 | } 1383 | } 1384 | }, 1385 | "retry": { 1386 | "version": "0.8.0" 1387 | } 1388 | } 1389 | }, 1390 | "npm-user-validate": { 1391 | "version": "0.1.2" 1392 | }, 1393 | "npmlog": { 1394 | "version": "2.0.2", 1395 | "dependencies": { 1396 | "are-we-there-yet": { 1397 | "version": "1.0.6", 1398 | "dependencies": { 1399 | "delegates": { 1400 | "version": "1.0.0" 1401 | } 1402 | } 1403 | }, 1404 | "gauge": { 1405 | "version": "1.2.5", 1406 | "dependencies": { 1407 | "has-unicode": { 1408 | "version": "2.0.0" 1409 | }, 1410 | "lodash.pad": { 1411 | "version": "3.2.2", 1412 | "dependencies": { 1413 | "lodash.repeat": { 1414 | "version": "3.1.2" 1415 | } 1416 | } 1417 | }, 1418 | "lodash.padleft": { 1419 | "version": "3.1.1", 1420 | "dependencies": { 1421 | "lodash._basetostring": { 1422 | "version": "3.0.1" 1423 | }, 1424 | "lodash._createpadding": { 1425 | "version": "3.6.1", 1426 | "dependencies": { 1427 | "lodash.repeat": { 1428 | "version": "3.1.2" 1429 | } 1430 | } 1431 | } 1432 | } 1433 | }, 1434 | "lodash.padright": { 1435 | "version": "3.1.1", 1436 | "dependencies": { 1437 | "lodash._basetostring": { 1438 | "version": "3.0.1" 1439 | }, 1440 | "lodash._createpadding": { 1441 | "version": "3.6.1", 1442 | "dependencies": { 1443 | "lodash.repeat": { 1444 | "version": "3.1.2" 1445 | } 1446 | } 1447 | } 1448 | } 1449 | } 1450 | } 1451 | } 1452 | } 1453 | }, 1454 | "once": { 1455 | "version": "1.3.3" 1456 | }, 1457 | "opener": { 1458 | "version": "1.4.1" 1459 | }, 1460 | "osenv": { 1461 | "version": "0.1.3", 1462 | "dependencies": { 1463 | "os-homedir": { 1464 | "version": "1.0.0" 1465 | }, 1466 | "os-tmpdir": { 1467 | "version": "1.0.1" 1468 | } 1469 | } 1470 | }, 1471 | "path-is-inside": { 1472 | "version": "1.0.1" 1473 | }, 1474 | "read": { 1475 | "version": "1.0.7", 1476 | "dependencies": { 1477 | "mute-stream": { 1478 | "version": "0.0.5" 1479 | } 1480 | } 1481 | }, 1482 | "read-installed": { 1483 | "version": "4.0.3", 1484 | "dependencies": { 1485 | "debuglog": { 1486 | "version": "1.0.1" 1487 | }, 1488 | "readdir-scoped-modules": { 1489 | "version": "1.0.2" 1490 | }, 1491 | "util-extend": { 1492 | "version": "1.0.1" 1493 | } 1494 | } 1495 | }, 1496 | "read-package-json": { 1497 | "version": "2.0.3", 1498 | "dependencies": { 1499 | "glob": { 1500 | "version": "6.0.4", 1501 | "dependencies": { 1502 | "path-is-absolute": { 1503 | "version": "1.0.0" 1504 | } 1505 | } 1506 | }, 1507 | "json-parse-helpfulerror": { 1508 | "version": "1.0.3", 1509 | "dependencies": { 1510 | "jju": { 1511 | "version": "1.2.1" 1512 | } 1513 | } 1514 | } 1515 | } 1516 | }, 1517 | "readable-stream": { 1518 | "version": "1.1.13", 1519 | "dependencies": { 1520 | "core-util-is": { 1521 | "version": "1.0.1" 1522 | }, 1523 | "isarray": { 1524 | "version": "0.0.1" 1525 | }, 1526 | "string_decoder": { 1527 | "version": "0.10.31" 1528 | } 1529 | } 1530 | }, 1531 | "realize-package-specifier": { 1532 | "version": "3.0.1" 1533 | }, 1534 | "request": { 1535 | "version": "2.69.0", 1536 | "dependencies": { 1537 | "aws-sign2": { 1538 | "version": "0.6.0" 1539 | }, 1540 | "aws4": { 1541 | "version": "1.2.1", 1542 | "dependencies": { 1543 | "lru-cache": { 1544 | "version": "2.7.3" 1545 | } 1546 | } 1547 | }, 1548 | "bl": { 1549 | "version": "1.0.2", 1550 | "dependencies": { 1551 | "readable-stream": { 1552 | "version": "2.0.5", 1553 | "dependencies": { 1554 | "core-util-is": { 1555 | "version": "1.0.2" 1556 | }, 1557 | "isarray": { 1558 | "version": "0.0.1" 1559 | }, 1560 | "process-nextick-args": { 1561 | "version": "1.0.6" 1562 | }, 1563 | "string_decoder": { 1564 | "version": "0.10.31" 1565 | }, 1566 | "util-deprecate": { 1567 | "version": "1.0.2" 1568 | } 1569 | } 1570 | } 1571 | } 1572 | }, 1573 | "caseless": { 1574 | "version": "0.11.0" 1575 | }, 1576 | "combined-stream": { 1577 | "version": "1.0.5", 1578 | "dependencies": { 1579 | "delayed-stream": { 1580 | "version": "1.0.0" 1581 | } 1582 | } 1583 | }, 1584 | "extend": { 1585 | "version": "3.0.0" 1586 | }, 1587 | "forever-agent": { 1588 | "version": "0.6.1" 1589 | }, 1590 | "form-data": { 1591 | "version": "1.0.0-rc3", 1592 | "dependencies": { 1593 | "async": { 1594 | "version": "1.5.2" 1595 | } 1596 | } 1597 | }, 1598 | "har-validator": { 1599 | "version": "2.0.6", 1600 | "dependencies": { 1601 | "chalk": { 1602 | "version": "1.1.1", 1603 | "dependencies": { 1604 | "ansi-styles": { 1605 | "version": "2.1.0" 1606 | }, 1607 | "escape-string-regexp": { 1608 | "version": "1.0.4" 1609 | }, 1610 | "has-ansi": { 1611 | "version": "2.0.0" 1612 | }, 1613 | "supports-color": { 1614 | "version": "2.0.0" 1615 | } 1616 | } 1617 | }, 1618 | "commander": { 1619 | "version": "2.9.0", 1620 | "dependencies": { 1621 | "graceful-readlink": { 1622 | "version": "1.0.1" 1623 | } 1624 | } 1625 | }, 1626 | "is-my-json-valid": { 1627 | "version": "2.12.4", 1628 | "dependencies": { 1629 | "generate-function": { 1630 | "version": "2.0.0" 1631 | }, 1632 | "generate-object-property": { 1633 | "version": "1.2.0", 1634 | "dependencies": { 1635 | "is-property": { 1636 | "version": "1.0.2" 1637 | } 1638 | } 1639 | }, 1640 | "jsonpointer": { 1641 | "version": "2.0.0" 1642 | }, 1643 | "xtend": { 1644 | "version": "4.0.1" 1645 | } 1646 | } 1647 | }, 1648 | "pinkie-promise": { 1649 | "version": "2.0.0", 1650 | "dependencies": { 1651 | "pinkie": { 1652 | "version": "2.0.4" 1653 | } 1654 | } 1655 | } 1656 | } 1657 | }, 1658 | "hawk": { 1659 | "version": "3.1.3", 1660 | "dependencies": { 1661 | "hoek": { 1662 | "version": "2.16.3" 1663 | }, 1664 | "boom": { 1665 | "version": "2.10.1" 1666 | }, 1667 | "cryptiles": { 1668 | "version": "2.0.5" 1669 | }, 1670 | "sntp": { 1671 | "version": "1.0.9" 1672 | } 1673 | } 1674 | }, 1675 | "http-signature": { 1676 | "version": "1.1.1", 1677 | "dependencies": { 1678 | "assert-plus": { 1679 | "version": "0.2.0" 1680 | }, 1681 | "jsprim": { 1682 | "version": "1.2.2", 1683 | "dependencies": { 1684 | "extsprintf": { 1685 | "version": "1.0.2" 1686 | }, 1687 | "json-schema": { 1688 | "version": "0.2.2" 1689 | }, 1690 | "verror": { 1691 | "version": "1.3.6" 1692 | } 1693 | } 1694 | }, 1695 | "sshpk": { 1696 | "version": "1.7.3", 1697 | "dependencies": { 1698 | "asn1": { 1699 | "version": "0.2.3" 1700 | }, 1701 | "dashdash": { 1702 | "version": "1.12.2" 1703 | }, 1704 | "jsbn": { 1705 | "version": "0.1.0" 1706 | }, 1707 | "tweetnacl": { 1708 | "version": "0.13.3" 1709 | }, 1710 | "jodid25519": { 1711 | "version": "1.0.2" 1712 | }, 1713 | "ecc-jsbn": { 1714 | "version": "0.1.1" 1715 | } 1716 | } 1717 | } 1718 | } 1719 | }, 1720 | "is-typedarray": { 1721 | "version": "1.0.0" 1722 | }, 1723 | "isstream": { 1724 | "version": "0.1.2" 1725 | }, 1726 | "json-stringify-safe": { 1727 | "version": "5.0.1" 1728 | }, 1729 | "mime-types": { 1730 | "version": "2.1.9", 1731 | "dependencies": { 1732 | "mime-db": { 1733 | "version": "1.21.0" 1734 | } 1735 | } 1736 | }, 1737 | "node-uuid": { 1738 | "version": "1.4.7" 1739 | }, 1740 | "oauth-sign": { 1741 | "version": "0.8.1" 1742 | }, 1743 | "qs": { 1744 | "version": "6.0.2" 1745 | }, 1746 | "stringstream": { 1747 | "version": "0.0.5" 1748 | }, 1749 | "tough-cookie": { 1750 | "version": "2.2.1" 1751 | }, 1752 | "tunnel-agent": { 1753 | "version": "0.4.2" 1754 | } 1755 | } 1756 | }, 1757 | "retry": { 1758 | "version": "0.9.0" 1759 | }, 1760 | "rimraf": { 1761 | "version": "2.5.1", 1762 | "dependencies": { 1763 | "glob": { 1764 | "version": "6.0.4", 1765 | "dependencies": { 1766 | "path-is-absolute": { 1767 | "version": "1.0.0" 1768 | } 1769 | } 1770 | } 1771 | } 1772 | }, 1773 | "semver": { 1774 | "version": "5.1.0" 1775 | }, 1776 | "sha": { 1777 | "version": "2.0.1", 1778 | "dependencies": { 1779 | "readable-stream": { 1780 | "version": "2.0.2", 1781 | "dependencies": { 1782 | "core-util-is": { 1783 | "version": "1.0.1" 1784 | }, 1785 | "isarray": { 1786 | "version": "0.0.1" 1787 | }, 1788 | "process-nextick-args": { 1789 | "version": "1.0.3" 1790 | }, 1791 | "string_decoder": { 1792 | "version": "0.10.31" 1793 | }, 1794 | "util-deprecate": { 1795 | "version": "1.0.1" 1796 | } 1797 | } 1798 | } 1799 | } 1800 | }, 1801 | "slide": { 1802 | "version": "1.1.6" 1803 | }, 1804 | "sorted-object": { 1805 | "version": "1.0.0" 1806 | }, 1807 | "spdx-license-ids": { 1808 | "version": "1.2.0" 1809 | }, 1810 | "tar": { 1811 | "version": "2.2.1" 1812 | }, 1813 | "text-table": { 1814 | "version": "0.2.0" 1815 | }, 1816 | "uid-number": { 1817 | "version": "0.0.6" 1818 | }, 1819 | "umask": { 1820 | "version": "1.1.0" 1821 | }, 1822 | "validate-npm-package-license": { 1823 | "version": "3.0.1", 1824 | "dependencies": { 1825 | "spdx-correct": { 1826 | "version": "1.0.2" 1827 | }, 1828 | "spdx-expression-parse": { 1829 | "version": "1.0.2", 1830 | "dependencies": { 1831 | "spdx-exceptions": { 1832 | "version": "1.0.4" 1833 | } 1834 | } 1835 | } 1836 | } 1837 | }, 1838 | "validate-npm-package-name": { 1839 | "version": "2.2.2", 1840 | "dependencies": { 1841 | "builtins": { 1842 | "version": "0.0.7" 1843 | } 1844 | } 1845 | }, 1846 | "which": { 1847 | "version": "1.2.4", 1848 | "dependencies": { 1849 | "is-absolute": { 1850 | "version": "0.1.7", 1851 | "dependencies": { 1852 | "is-relative": { 1853 | "version": "0.1.3" 1854 | } 1855 | } 1856 | }, 1857 | "isexe": { 1858 | "version": "1.1.1" 1859 | } 1860 | } 1861 | }, 1862 | "wrappy": { 1863 | "version": "1.0.1" 1864 | }, 1865 | "write-file-atomic": { 1866 | "version": "1.1.4" 1867 | }, 1868 | "ansi-regex": { 1869 | "version": "2.0.0" 1870 | }, 1871 | "imurmurhash": { 1872 | "version": "0.1.4" 1873 | }, 1874 | "strip-ansi": { 1875 | "version": "3.0.0" 1876 | } 1877 | } 1878 | }, 1879 | "nslog": { 1880 | "version": "3.0.0", 1881 | "dependencies": { 1882 | "nan": { 1883 | "version": "2.2.0" 1884 | } 1885 | } 1886 | }, 1887 | "promise": { 1888 | "version": "7.1.1", 1889 | "dependencies": { 1890 | "asap": { 1891 | "version": "2.0.3" 1892 | } 1893 | } 1894 | }, 1895 | "yargs": { 1896 | "version": "3.32.0", 1897 | "dependencies": { 1898 | "camelcase": { 1899 | "version": "2.1.0" 1900 | }, 1901 | "cliui": { 1902 | "version": "3.1.0", 1903 | "dependencies": { 1904 | "strip-ansi": { 1905 | "version": "3.0.0", 1906 | "dependencies": { 1907 | "ansi-regex": { 1908 | "version": "2.0.0" 1909 | } 1910 | } 1911 | }, 1912 | "wrap-ansi": { 1913 | "version": "1.0.0" 1914 | } 1915 | } 1916 | }, 1917 | "decamelize": { 1918 | "version": "1.1.2", 1919 | "dependencies": { 1920 | "escape-string-regexp": { 1921 | "version": "1.0.4" 1922 | } 1923 | } 1924 | }, 1925 | "os-locale": { 1926 | "version": "1.4.0", 1927 | "dependencies": { 1928 | "lcid": { 1929 | "version": "1.0.0", 1930 | "dependencies": { 1931 | "invert-kv": { 1932 | "version": "1.0.0" 1933 | } 1934 | } 1935 | } 1936 | } 1937 | }, 1938 | "string-width": { 1939 | "version": "1.0.1", 1940 | "dependencies": { 1941 | "code-point-at": { 1942 | "version": "1.0.0", 1943 | "dependencies": { 1944 | "number-is-nan": { 1945 | "version": "1.0.0" 1946 | } 1947 | } 1948 | }, 1949 | "is-fullwidth-code-point": { 1950 | "version": "1.0.0", 1951 | "dependencies": { 1952 | "number-is-nan": { 1953 | "version": "1.0.0" 1954 | } 1955 | } 1956 | }, 1957 | "strip-ansi": { 1958 | "version": "3.0.0", 1959 | "dependencies": { 1960 | "ansi-regex": { 1961 | "version": "2.0.0" 1962 | } 1963 | } 1964 | } 1965 | } 1966 | }, 1967 | "window-size": { 1968 | "version": "0.1.4" 1969 | }, 1970 | "y18n": { 1971 | "version": "3.2.0" 1972 | } 1973 | } 1974 | } 1975 | } 1976 | }, 1977 | "is-running": { 1978 | "version": "1.0.5" 1979 | }, 1980 | "lucy-dirsum": { 1981 | "version": "https://github.com/mixmaxhq/lucy-dirsum/archive/08299b483cd0f79d18cd0fa1c5081dcab67c5649.tar.gz" 1982 | }, 1983 | "mkdirp": { 1984 | "version": "0.5.1", 1985 | "dependencies": { 1986 | "minimist": { 1987 | "version": "0.0.8" 1988 | } 1989 | } 1990 | }, 1991 | "ncp": { 1992 | "version": "2.0.0" 1993 | }, 1994 | "rimraf": { 1995 | "version": "2.4.4", 1996 | "dependencies": { 1997 | "glob": { 1998 | "version": "5.0.15", 1999 | "dependencies": { 2000 | "inflight": { 2001 | "version": "1.0.4", 2002 | "dependencies": { 2003 | "wrappy": { 2004 | "version": "1.0.1" 2005 | } 2006 | } 2007 | }, 2008 | "inherits": { 2009 | "version": "2.0.1" 2010 | }, 2011 | "minimatch": { 2012 | "version": "3.0.0", 2013 | "dependencies": { 2014 | "brace-expansion": { 2015 | "version": "1.1.2", 2016 | "dependencies": { 2017 | "balanced-match": { 2018 | "version": "0.3.0" 2019 | }, 2020 | "concat-map": { 2021 | "version": "0.0.1" 2022 | } 2023 | } 2024 | } 2025 | } 2026 | }, 2027 | "once": { 2028 | "version": "1.3.3", 2029 | "dependencies": { 2030 | "wrappy": { 2031 | "version": "1.0.1" 2032 | } 2033 | } 2034 | }, 2035 | "path-is-absolute": { 2036 | "version": "1.0.0" 2037 | } 2038 | } 2039 | } 2040 | } 2041 | }, 2042 | "semver": { 2043 | "version": "5.1.0" 2044 | }, 2045 | "url-join": { 2046 | "version": "0.0.1" 2047 | } 2048 | } 2049 | } 2050 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | callback-hook@1.0.4 9 | check@1.1.0 10 | ddp@1.2.2 11 | ddp-client@1.2.1 12 | ddp-common@1.2.2 13 | ddp-server@1.2.2 14 | deps@1.0.9 15 | diff-sequence@1.0.1 16 | ecmascript@0.1.6 17 | ecmascript-runtime@0.2.6 18 | ejson@1.0.7 19 | geojson-utils@1.0.4 20 | html-tools@1.0.5 21 | htmljs@1.0.5 22 | id-map@1.0.4 23 | jquery@1.11.4 24 | local-test:meson:electron@0.1.3 25 | logging@1.0.8 26 | meson:electron@0.1.3 27 | meteor@1.1.10 28 | minimongo@1.0.10 29 | mongo@1.1.3 30 | mongo-id@1.0.1 31 | mongo-livedata@1.0.9 32 | npm-mongo@1.4.39_1 33 | observe-sequence@1.0.7 34 | ordered-dict@1.0.4 35 | promise@0.5.1 36 | random@1.0.5 37 | reactive-var@1.0.6 38 | retry@1.0.4 39 | routepolicy@1.0.6 40 | spacebars@1.0.7 41 | spacebars-compiler@1.0.7 42 | tinytest@1.0.6 43 | tracker@1.0.9 44 | ui@1.0.8 45 | underscore@1.0.4 46 | webapp@1.2.3 47 | webapp-hashing@1.0.5 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michael Risse 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 | # meteor-electron 2 | 3 | meteor-electron lets you easily transform your Meteor webapp to a desktop app. Its ultimate goal is 4 | to build `meteor add-platform desktop`. 5 | 6 | Some of the things it does: 7 | 8 | * automatically builds and launches a desktop application, rebuilding when the native code changes 9 | * defines feature detection APIs and a bridge between web and native code 10 | * serves downloads of your application and update feeds 11 | 12 | ![](docs/overview.png) 13 | 14 | ## Getting Started 15 | 16 | `meteor add meson:electron` 17 | 18 | meteor-electron will download the Electron binary for your system and build and launch an Electron 19 | app pointing to your local development server. The download process may take a few minutes based on 20 | your Internet connection but only needs to be done once. 21 | 22 | The app, as well as the ready-to-distribute binaries (see [Deploy](#deploy)), is built within 23 | `YOUR_PROJECT_DIRECTORY/.meteor-electron`. This allows the apps to be easily located as well as the 24 | builds to be cached for speedier startup. You should add this directory to your `.gitignore`. 25 | 26 | ## Configuration 27 | 28 | Configuration is possible via `Meteor.settings.electron`. For example, 29 | 30 | ```json 31 | { 32 | "electron": { 33 | "name": "MyApp", 34 | "icon": { 35 | "darwin": "private/MyApp.icns", 36 | "win32": "private/MyApp.ico" 37 | }, 38 | "version": "0.1.0", 39 | "description": "A really cool app.", 40 | "rootUrl": "https://myapp.com", 41 | "launchPath": "/app/landing", 42 | "downloadUrls": { 43 | "win32": "https://myapp.com/download/win/", 44 | "darwin": "https://myapp.com/download/osx/{{version}}/MyApp.zip" 45 | }, 46 | "sign": "Developer ID Application: ...", 47 | "height": 768, 48 | "width": 1024, 49 | "frame": true, 50 | "title-bar-style": "hidden", 51 | "resizable": true, 52 | "protocols": [{ 53 | "name": "MyApp", 54 | "schemes": ["myapp"] 55 | }], 56 | "appSrcDir": "private/app" 57 | } 58 | } 59 | ``` 60 | 61 |
62 |
icon
63 |
platform dependent icon paths relative to application root
64 |
version
65 |
must confirm to semver
66 |
rootUrl
67 |
If unset, defaults to the `APP_ROOT_URL` and then `ROOT_URL` environment variables, in that order.
68 |
launchPath
69 |
If you want your app to open to a non-root URL. Will be appended to the root URL.
70 |
downloadUrls
71 |
URLs from which downloads are served. A CDN is recommended, but any HTTP server will do.
72 |
downloadUrls.win32
73 |
Copy the output of `grunt-electron-installer` (see Building and serving an auto-updating Windows app) to this location. Do not rename the files. If you wish to host the Windows 74 | installers at versioned URLs for caching or archival reasons, specify this as an object with the 75 | following keys.
76 |
downloadUrls.win32.releases
77 |
Copy the output of `grunt-electron-installer` (see Building and serving an auto-updating Windows app) to this location. Do not rename the files.
78 |
downloadUrls.win32.installer
79 |
If you like, you may copy the `Setup.exe` file created by `grunt-electron-installer` to this 80 | location rather than the "releases" location. If the URL contains '{{version}}', it will be 81 | replaced with `version`.
82 |
downloadUrls.darwin
83 |
Place the latest app at this location. If the URL contains '{{version}}', it will be replaced 84 | with `version`.
85 |
sign
86 |
Must be set to enable auto-updates on Mac.
87 |
appSrcDir
88 |
A directory of code to use instead of meteor-electron's default application, relative to your 89 | app's project directory. See warning below.
90 |
91 | 92 | ## Electron-specific code 93 | 94 | By default, all client web code will be executed in Electron. To include/exclude code use `Electron.isDesktop` 95 | 96 | ```javascript 97 | if (!Electron.isDesktop()){ 98 | showModal("Have you considered downloading our Electron app?"); 99 | } 100 | ``` 101 | 102 | ## Deploying 103 | 104 | Hot code push will work to update your app's UI just like it does on the web, since the app is loading the UI 105 | _from_ the web. If you want to update the part of the app that interfaces with the OS, though—to change 106 | the app icon, to add a menu bar icon, etc.—you'll need to distribute a new version of the `.app` or 107 | `.exe`. Here's how to do that. 108 | 109 | ### Building and serving an auto-updating Mac app 110 | 111 | 1. Set `Meteor.settings.electron.autoPackage` to `true` to ZIP your app for distribution after it is 112 | built. 113 | 2. If you wish to enable remote updates, you will need to codesign your application. This requires 114 | that you build your app on a Mac with a [Developer ID certificate](https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/AppDistributionGuide/DistributingApplicationsOutside/DistributingApplicationsOutside.html) installed. 115 | Set `Meteor.settings.electron.sign` to the name of that certificate. 116 | 3. Wait for the app to finish building and packaging, then copy 117 | `YOUR_PROJECT_DIRECTORY/.meteor-electron/darwin-x64/final/YOUR_APP_NAME.zip` to a publically-accessible 118 | location. 119 | 4. Set `downloadUrls.darwin` in `Meteor.settings.electron` to the URL of the location where you copied the ZIP. 120 | 121 | Downloads of the Mac app will be served at your webapp's `ROOT_URL` + `/app/download?platform=darwin`. 122 | 123 | ### Building and serving an auto-updating Windows app 124 | 125 | 0. Make sure that you have specified `name`, `version`, and `description` in `Meteor.settings.electron`. 126 | 1. Build the app [on a Mac](#building-for-windows-on-mac), because changing a Windows application icon 127 | [does not work on Windows at present](https://github.com/maxogden/electron-packager/issues/53). 128 | 2. Ensure the URL specified by `Meteor.settings.electron.downloadUrls.win32` has an empty `RELEASES` file. 129 | 2. On a Windows machine or in a Windows VM ([not a Mac, at present](https://github.com/atom/grunt-electron-installer/issues/90)), 130 | run the [electron installer grunt plugin](https://github.com/atom/grunt-electron-installer) against your app. 131 | Your Gruntfile should look something like https://github.com/rissem/meteor-electron-test/tree/master/.test. 132 | The value of `remoteReleases` should be your webapp's `ROOT_URL` + '/app/latest'. 133 | 3. Copy the output to the server serving `Meteor.settings.electron.downloadUrls.win32`, to be served 134 | from that location. 135 | 4. When you publish a new update, run the installer again and it will generate diffs, a new `RELEASES` file, 136 | and new installers. After copying these to `Meteor.settings.electron.downloadUrls.win32` again (overwriting 137 | the `RELEASES` file and installers), apps that check for updates should receive a new version. 138 | 139 | Downloads of the Windows installer will be served at your webapp's `ROOT_URL` + `/app/download?platform=win32`. 140 | 141 | ## Building for Windows on Mac 142 | 143 | 1. Install [homebrew](http://brew.sh/) 144 | 2. `brew update` 145 | 3. `brew install wine` 146 | 4. Specify a Windows build in your settings (otherwise defaults to current platform (mac)). 147 | 148 | ```json 149 | { 150 | "electron": { 151 | "builds": [ 152 | {"platform": "win32", 153 | "arch": "ia32"} 154 | ] 155 | } 156 | } 157 | ``` 158 | 159 | ## Example 160 | 161 | [TODO] Link to an awesome chat app 162 | 163 | ## Q&A 164 | 165 | ### Q: How is this different from all the other Meteor electron packages? 166 | 167 | This package differs from [Electrometeor](https://github.com/sircharleswatson/Electrometeor) and 168 | [Electrify](https://github.com/arboleya/electrify) by *not* baking Meteor into the packaged app. 169 | This makes things significantly simpler, but if you need strong offline support, one of them is a 170 | better solution. 171 | 172 | ### Q: How can I create new browser windows, set app notifications and all the other awesome native functionality that Electron gives me? 173 | 174 | This project selectively exposes such functionality to the client, in a way that is safe and avoids 175 | memory leaks, via the `Electron` module--see [`client.js`](client.js). To request that this module 176 | expose additional functionality, please [submit a pull request](https://github.com/rissem/meteor-electron/pull/new/master) 177 | or [file an issue](https://github.com/rissem/meteor-electron/issues/new). 178 | 179 | You may also substitute your own application code for `meteor-electron`'s default application by 180 | setting the `appSrcDir` settings option. `meteor-electron` will continue to package your application 181 | and serve the application update feed and download URLs, but in-app functionality will be your 182 | responsibility. **Warning**: this responsibility includes setting up your application window and menu, 183 | checking for remote updates, registering the `Electron` module (that defines `Electron.isDesktop`), 184 | and possibly other things. If you take this route, it's recommended that you start by copying 185 | `meteor-electron`'s `app` directory. 186 | 187 | Also, you also probably want to keep your application code in a subdirectory of your application's 188 | `private` directory so that Meteor will observe changes to it and restart the server; when it does 189 | so, `meteor-electron` will rebuild and relaunch the app. 190 | 191 | ### Q: How do I prevent the Electron app from being automatically built and launched? 192 | 193 | Set `Meteor.settings.electron.autoBuild` to `"false"`. 194 | -------------------------------------------------------------------------------- /app/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // If you are using SublimeLinter, after modifying this config file, be sure to close and reopen 3 | // the file(s) you were editing to see the changes take effect. 4 | 5 | "extends": "../.jshintrc", 6 | 7 | // Whitelist Node's global variables so JSHint doesn't complain about them being undefined. 8 | "node": true, 9 | 10 | // Whitelist additional global variables. 11 | "globals": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/autoUpdater.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var app = require('electron').app; 3 | var autoUpdater = require('electron').autoUpdater; 4 | var dialog = require('electron').dialog; 5 | 6 | // Daily. 7 | var SCHEDULED_CHECK_INTERVAL = 24 * 60 * 60 * 1000; 8 | 9 | var Updater = function() { 10 | autoUpdater.on('error', this._onUpdateError.bind(this)); 11 | autoUpdater.on('update-not-available', this._onUpdateNotAvailable.bind(this)); 12 | autoUpdater.on('update-downloaded', this._onUpdateDownloaded.bind(this)); 13 | }; 14 | 15 | _.extend(Updater.prototype, { 16 | setFeedURL: function(url) { 17 | autoUpdater.setFeedURL(url); 18 | }, 19 | 20 | checkForUpdates: function(userTriggered /* optional */) { 21 | // Asking the updater to check while it's already checking may result in an error. 22 | if (this._checkPending) return; 23 | 24 | this._clearScheduledCheck(); 25 | if (this._updatePending) { 26 | this._askToApplyUpdate(); 27 | return; 28 | } 29 | 30 | this._checkPending = true; 31 | if (userTriggered) this._userCheckPending = true; 32 | 33 | autoUpdater.checkForUpdates(); 34 | }, 35 | 36 | _onUpdateError: function() { 37 | this._checkPending = false; 38 | if (this._userCheckPending) { 39 | this._userCheckPending = false; 40 | 41 | dialog.showMessageBox({ 42 | type: 'error', 43 | message: 'An error occurred while checking for updates.', 44 | buttons: ['Ok'] 45 | }); 46 | } 47 | 48 | this._scheduleCheck(); 49 | }, 50 | 51 | _onUpdateNotAvailable: function() { 52 | this._checkPending = false; 53 | if (this._userCheckPending) { 54 | this._userCheckPending = false; 55 | 56 | dialog.showMessageBox({ 57 | type: 'info', 58 | message: 'An update is not available.', 59 | buttons: ['Ok'] 60 | }); 61 | } 62 | 63 | this._scheduleCheck(); 64 | }, 65 | 66 | _onUpdateDownloaded: function() { 67 | this._checkPending = false; 68 | this._userCheckPending = false; 69 | this._updatePending = true; 70 | this._askToApplyUpdate(); 71 | }, 72 | 73 | _askToApplyUpdate: function() { 74 | var self = this; 75 | 76 | dialog.showMessageBox({ 77 | type: 'question', 78 | message: 'An update is available! Would you like to quit to install it? The application will then restart.', 79 | buttons: ['Ask me later', 'Quit and install'] 80 | }, function(result) { 81 | if (result > 0) { 82 | // Emit the 'before-quit' event since the app won't quit otherwise 83 | // (https://app.asana.com/0/19141607276671/74169390751974) and the app won't: 84 | // https://github.com/atom/electron/issues/3837 85 | var event = { 86 | _defaultPrevented: false, 87 | isDefaultPrevented: function() { 88 | return this._defaultPrevented; 89 | }, 90 | preventDefault: function() { 91 | this._defaultPrevented = true; 92 | } 93 | }; 94 | 95 | app.emit('before-quit', event); 96 | if (event.isDefaultPrevented()) return; 97 | 98 | autoUpdater.quitAndInstall(); 99 | } else { 100 | self._scheduleCheck(); 101 | } 102 | }); 103 | }, 104 | 105 | _clearScheduledCheck: function() { 106 | if (this._scheduledCheck) { 107 | clearTimeout(this._scheduledCheck); 108 | this._scheduledCheck = null; 109 | } 110 | }, 111 | 112 | _scheduleCheck: function() { 113 | this._clearScheduledCheck(); 114 | this._scheduledCheck = setTimeout(this.checkForUpdates.bind(this), SCHEDULED_CHECK_INTERVAL); 115 | } 116 | }); 117 | 118 | module.exports = new Updater(); 119 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | var app = require('electron').app; // Module to control application life. 2 | var childProcess = require("child_process"); 3 | var path = require("path"); 4 | var fs = require("fs"); 5 | 6 | // var log = function(msg){ 7 | // fs.appendFile("C:\\Users\\Michael\\electron.log", msg + "\n", function(err){ 8 | // if (err){ 9 | // throw err; 10 | // } 11 | // }) 12 | // }; 13 | 14 | var log = function(){}; 15 | 16 | var installShortcut = function(callback){ 17 | var updateDotExe = path.resolve(path.dirname(process.execPath), '..', 'update.exe'); 18 | var child = childProcess.spawn(updateDotExe, ["--createShortcut", "mixmax.exe"], { detached: true }); 19 | child.on('close', function(code) { 20 | callback(); 21 | }); 22 | }; 23 | 24 | var handleStartupEvent = function() { 25 | if (process.platform !== 'win32') { 26 | return false; 27 | } 28 | 29 | var squirrelCommand = process.argv[1]; 30 | switch (squirrelCommand) { 31 | case '--squirrel-install': 32 | log("SQUIRREL INSTALL"); 33 | 34 | case '--squirrel-updated': 35 | log("SQUIRREL UPDATED"); 36 | // Optionally do things such as: 37 | // 38 | // - Install desktop and start menu shortcuts 39 | // - Add your .exe to the PATH 40 | // - Write to the registry for things like file associations and 41 | // explorer context menus 42 | 43 | // Always quit when done 44 | installShortcut(function(){ 45 | app.quit(); 46 | }) 47 | 48 | return true; 49 | case '--squirrel-uninstall': 50 | log("SQUIRREL UNINSTALL"); 51 | 52 | // Undo anything you did in the --squirrel-install and 53 | // --squirrel-updated handlers 54 | 55 | // Always quit when done 56 | app.quit(); 57 | 58 | return true; 59 | case '--squirrel-obsolete': 60 | log("SQUIRREL OBSOLETE"); 61 | // This is called on the outgoing version of your app before 62 | // we update to the new version - it's the opposite of 63 | // --squirrel-updated 64 | app.quit(); 65 | return true; 66 | } 67 | }; 68 | 69 | app.on("window-all-closed", function(){ 70 | if (process.platform !== "darwin"){ 71 | app.quit(); 72 | } 73 | }) 74 | 75 | if (handleStartupEvent()) { 76 | return; 77 | } 78 | 79 | var BrowserWindow = require('electron').BrowserWindow; // Module to create native browser window. 80 | var autoUpdater = require('./autoUpdater'); 81 | var path = require("path"); 82 | var fs = require("fs"); 83 | var createDefaultMenu = require('./menu.js'); 84 | var proxyWindowEvents = require('./proxyWindowEvents'); 85 | 86 | require('electron-debug')({ 87 | showDevTools: false 88 | }); 89 | 90 | var electronSettings = JSON.parse(fs.readFileSync( 91 | path.join(__dirname, "electronSettings.json"), "utf-8")); 92 | 93 | var checkForUpdates; 94 | if (electronSettings.updateFeedUrl) { 95 | autoUpdater.setFeedURL(electronSettings.updateFeedUrl + '?version=' + electronSettings.version); 96 | autoUpdater.checkForUpdates(); 97 | checkForUpdates = function() { 98 | autoUpdater.checkForUpdates(true /* userTriggered */); 99 | }; 100 | } 101 | 102 | var launchUrl = electronSettings.rootUrl; 103 | if (electronSettings.launchPath) { 104 | launchUrl += electronSettings.launchPath; 105 | } 106 | 107 | var windowOptions = { 108 | width: electronSettings.width || 800, 109 | height: electronSettings.height || 600, 110 | resizable: true, 111 | frame: true, 112 | /** 113 | * Disable Electron's Node integration so that browser dependencies like `moment` will load themselves 114 | * like normal i.e. into the window rather than into modules, and also to prevent untrusted client 115 | * code from having access to the process and file system: 116 | * - https://github.com/atom/electron/issues/254 117 | * - https://github.com/atom/electron/issues/1753 118 | */ 119 | webPreferences: { 120 | nodeIntegration: false, 121 | // See comments at the top of `preload.js`. 122 | preload: path.join(__dirname, 'preload.js') 123 | } 124 | }; 125 | 126 | if (electronSettings.resizable === false){ 127 | windowOptions.resizable = false; 128 | } 129 | 130 | if (electronSettings['title-bar-style']) { 131 | windowOptions['title-bar-style'] = electronSettings['title-bar-style']; 132 | } 133 | 134 | if (electronSettings.minWidth) { 135 | windowOptions.minWidth = electronSettings.minWidth; 136 | } 137 | 138 | if (electronSettings.maxWidth) { 139 | windowOptions.maxWidth = electronSettings.maxWidth; 140 | } 141 | 142 | if (electronSettings.minHeight) { 143 | windowOptions.minHeight = electronSettings.minHeight; 144 | } 145 | 146 | if (electronSettings.maxHeight) { 147 | windowOptions.maxHeight = electronSettings.maxHeight; 148 | } 149 | 150 | if (electronSettings.frame === false){ 151 | windowOptions.frame = false; 152 | } 153 | 154 | // Keep a global reference of the window object so that it won't be garbage collected 155 | // and the window closed. 156 | var mainWindow = null; 157 | var getMainWindow = function() { 158 | return mainWindow; 159 | }; 160 | 161 | // Unfortunately, we must set the menu before the application becomes ready and so before the main 162 | // window is available to be passed directly to `createDefaultMenu`. 163 | createDefaultMenu(app, getMainWindow, checkForUpdates); 164 | 165 | app.on("ready", function(){ 166 | mainWindow = new BrowserWindow(windowOptions); 167 | proxyWindowEvents(mainWindow); 168 | 169 | // Hide the main window instead of closing it, so that we can bring it back 170 | // more quickly. 171 | mainWindow.on('close', hideInsteadofClose); 172 | 173 | mainWindow.focus(); 174 | mainWindow.loadURL(launchUrl); 175 | }); 176 | 177 | var hideInsteadofClose = function(e) { 178 | mainWindow.hide(); 179 | e.preventDefault(); 180 | }; 181 | 182 | app.on("before-quit", function(){ 183 | // We need to remove our close event handler from the main window, 184 | // otherwise the app will not quit. 185 | mainWindow.removeListener('close', hideInsteadofClose); 186 | }); 187 | 188 | app.on("activate", function(){ 189 | // Show the main window when the customer clicks on the app icon. 190 | if (!mainWindow.isVisible()) mainWindow.show(); 191 | }); 192 | -------------------------------------------------------------------------------- /app/menu.js: -------------------------------------------------------------------------------- 1 | var BrowserWindow = require('browser-window'); 2 | var Menu = require('menu'); 3 | 4 | /** 5 | * Creates a default menu. Modeled after https://github.com/atom/electron/pull/1863, augmented with 6 | * the roles from https://github.com/atom/electron/blob/master/docs/api/menu.md. 7 | */ 8 | var createDefaultMenu = function(app, getMainWindow, checkForUpdates) { 9 | app.once('ready', function() { 10 | var template; 11 | if (process.platform == 'darwin') { 12 | template = [ 13 | { 14 | label: app.getName(), 15 | submenu: [ 16 | { 17 | label: 'About ' + app.getName(), 18 | role: 'about', 19 | }, 20 | { 21 | type: 'separator' 22 | }, 23 | { 24 | label: 'Services', 25 | role: 'services', 26 | submenu: [] 27 | }, 28 | { 29 | type: 'separator' 30 | }, 31 | { 32 | label: 'Hide ' + app.getName(), 33 | accelerator: 'Command+H', 34 | role: 'hide' 35 | }, 36 | { 37 | label: 'Hide Others', 38 | accelerator: 'Command+Shift+H', 39 | role: 'hideothers' 40 | }, 41 | { 42 | label: 'Show All', 43 | role: 'unhide' 44 | }, 45 | { 46 | type: 'separator' 47 | }, 48 | { 49 | label: 'Quit', 50 | accelerator: 'Command+Q', 51 | click: function() { app.quit(); } 52 | }, 53 | ] 54 | }, 55 | { 56 | label: 'File', 57 | submenu: [ 58 | { 59 | label: 'Refresh', 60 | accelerator: 'Command+R', 61 | click: function() { 62 | var focusedWindow = BrowserWindow.getFocusedWindow(); 63 | if (focusedWindow) { 64 | focusedWindow.reload(); 65 | } 66 | } 67 | }, 68 | { 69 | label: 'Close', 70 | accelerator: 'Command+W', 71 | role: 'close' 72 | } 73 | ] 74 | }, 75 | { 76 | label: 'Edit', 77 | submenu: [ 78 | { 79 | label: 'Undo', 80 | accelerator: 'Command+Z', 81 | role: 'undo' 82 | }, 83 | { 84 | label: 'Redo', 85 | accelerator: 'Shift+Command+Z', 86 | role: 'redo' 87 | }, 88 | { 89 | type: 'separator' 90 | }, 91 | { 92 | label: 'Cut', 93 | accelerator: 'Command+X', 94 | role: 'cut' 95 | }, 96 | { 97 | label: 'Copy', 98 | accelerator: 'Command+C', 99 | role: 'copy' 100 | }, 101 | { 102 | label: 'Paste', 103 | accelerator: 'Command+V', 104 | role: 'paste' 105 | }, 106 | { 107 | label: 'Select All', 108 | accelerator: 'Command+A', 109 | role: 'selectall' 110 | }, 111 | ] 112 | }, 113 | { 114 | label: 'Window', 115 | submenu: [ 116 | { 117 | label: 'Minimize', 118 | accelerator: 'Command+M', 119 | role: 'minimize' 120 | }, 121 | { 122 | label: 'Toggle Full Screen', 123 | accelerator: 'Ctrl+Command+F', 124 | click: function() { 125 | var focusedWindow = BrowserWindow.getFocusedWindow(); 126 | if (focusedWindow) { 127 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); 128 | } 129 | } 130 | }, 131 | { 132 | type: 'separator' 133 | }, 134 | { 135 | label: 'Main Window', 136 | accelerator: 'Command+1', 137 | click: function() { 138 | var mainWindow = getMainWindow(); 139 | if (mainWindow) { 140 | mainWindow.show(); 141 | } 142 | } 143 | }, 144 | { 145 | type: 'separator' 146 | }, 147 | { 148 | label: 'Bring All to Front', 149 | role: 'front' 150 | }, 151 | ] 152 | } 153 | ]; 154 | 155 | if (checkForUpdates) { 156 | // Add 'Check for Updates' below the 'About' menu item. 157 | template[0].submenu.splice(1, 0, { 158 | label: 'Check for Updates', 159 | click: checkForUpdates 160 | }); 161 | } 162 | } else { 163 | template = [ 164 | { 165 | label: '&File', 166 | submenu: [ 167 | { 168 | label: '&Open', 169 | accelerator: 'Ctrl+O', 170 | }, 171 | { 172 | label: '&Refresh', 173 | accelerator: 'Ctrl+R', 174 | click: function() { 175 | var focusedWindow = BrowserWindow.getFocusedWindow(); 176 | if (focusedWindow) { 177 | focusedWindow.reload(); 178 | } 179 | } 180 | }, 181 | { 182 | label: '&Close', 183 | accelerator: 'Ctrl+W', 184 | click: function() { 185 | var focusedWindow = BrowserWindow.getFocusedWindow(); 186 | if (focusedWindow) { 187 | focusedWindow.close(); 188 | } 189 | } 190 | }, 191 | ] 192 | }, 193 | { 194 | label: '&Window', 195 | submenu: [ 196 | { 197 | label: 'Toggle &Full Screen', 198 | accelerator: 'F11', 199 | click: function() { 200 | var focusedWindow = BrowserWindow.getFocusedWindow(); 201 | if (focusedWindow) { 202 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); 203 | } 204 | } 205 | } 206 | ] 207 | } 208 | ]; 209 | 210 | if (checkForUpdates) { 211 | // Add a separator and 'Check for Updates' at the bottom of the 'File' menu. 212 | template[0].submenu.push({ 213 | type: 'separator' 214 | }, { 215 | label: '&Check for Updates', 216 | click: checkForUpdates 217 | }); 218 | } 219 | } 220 | 221 | var menu = Menu.buildFromTemplate(template); 222 | Menu.setApplicationMenu(menu); 223 | }); 224 | }; 225 | 226 | module.exports = createDefaultMenu; 227 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron", 3 | "productName": "Electron", 4 | "main": "main.js", 5 | "dependencies": { 6 | "electron-debug": "^0.5.1", 7 | "underscore": "^1.8.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/preload.js: -------------------------------------------------------------------------------- 1 | /* global ElectronImplementation:true */ 2 | 3 | /** 4 | * Since we've disabled Node integration in the browser window, we must selectively expose 5 | * main-process/Node modules via this script. 6 | * 7 | * @WARNING This file must take care not to leak the imported modules to the browser window! 8 | * In particular, do not save the following variables as properties of `ElectronImplementation`. 9 | * See https://github.com/atom/electron/issues/1753#issuecomment-104719851. 10 | */ 11 | var _ = require('underscore'); 12 | var ipc = require('electron').ipcRenderer; 13 | var remote = require('electron').remote; 14 | var shell = require('electron').shell; 15 | 16 | /** 17 | * Defines methods with which to extend the `Electron` module defined in `client.js`. 18 | * This must be a global in order to escape the preload script and be available to `client.js`. 19 | */ 20 | ElectronImplementation = { 21 | /** 22 | * Open the given external protocol URL in the desktop's default manner. (For example, http(s): 23 | * URLs in the user's default browser.) 24 | * 25 | * @param {String} url - The URL to open. 26 | */ 27 | openExternal: shell.openExternal, 28 | 29 | /** 30 | * Determines if the browser window is currently in fullscreen mode. 31 | * 32 | * "Fullscreen" here refers to the state triggered by toggling the native controls, not that 33 | * toggled by the HTML API. 34 | * 35 | * To detect when the browser window changes fullscreen state, observe the 'enter-full-screen' 36 | * and 'leave-full-screen' events using `onWindowEvent`. 37 | * 38 | * @return {Boolean} `true` if the browser window is in fullscreen mode, `false` otherwise. 39 | */ 40 | isFullScreen: function() { 41 | return remote.getCurrentWindow().isFullScreen(); 42 | }, 43 | 44 | /** 45 | * Invokes _callback_ when the specified `BrowserWindow` event is fired. 46 | * 47 | * This differs from `onEvent` in that it directs Electron to start emitting the relevant window 48 | * event. 49 | * 50 | * See https://github.com/atom/electron/blob/master/docs/api/browser-window.md#events for a list 51 | * of events. 52 | * 53 | * The implementation of this API, in particular the use of the `ipc` vs. `remote` modules, is 54 | * designed to avoid memory leaks as described by 55 | * https://github.com/atom/electron/blob/master/docs/api/remote.md#passing-callbacks-to-the-main-process. 56 | * 57 | * @param {String} event - The name of a `BrowserWindow` event. 58 | * @param {Function} callback - A function to invoke when `event` is triggered. Takes no arguments 59 | * and returns no value. 60 | */ 61 | onWindowEvent: function(event, callback) { 62 | this.onEvent(event, callback); 63 | ipc.send('observe-window-event', event); 64 | }, 65 | 66 | /** 67 | * Invokes _callback_ when the specified IPC event is fired. 68 | * 69 | * @param {String} event - The name of an event. 70 | * @param {Function} callback - A function to invoke when `event` is triggered. Takes no arguments 71 | * and returns no value. 72 | */ 73 | onEvent: function(event, callback) { 74 | var listeners = this._eventListeners[event]; 75 | if (!listeners) { 76 | listeners = this._eventListeners[event] = []; 77 | ipc.on(event, function() { 78 | _.invoke(listeners, 'call'); 79 | }); 80 | } 81 | listeners.push(callback); 82 | }, 83 | 84 | _eventListeners: {} 85 | }; 86 | -------------------------------------------------------------------------------- /app/proxyWindowEvents.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var ipc = require('ipc-main'); 3 | 4 | /** 5 | * Proxies `BrowserWindow` events to renderer processes as directed by those processes and in a way 6 | * that avoids memory leaks. 7 | * 8 | * For each event that the renderer process wishes to observe, it should send the 'onWindowEvent' 9 | * message with the event name as argument: 10 | * 11 | * require('electron').ipcRenderer.send('onWindowEvent', 'enter-full-screen') 12 | * 13 | * The renderer process will then receive the 'triggerWindowEvent' message when the event occurs: 14 | * 15 | * require('electron').ipcRenderer.on('triggerWindowEvent', function(event, arg) { 16 | * console.log(arg); // prints 'enter-full-screen' 17 | * }); 18 | * 19 | * This module, in particular the use of the `ipc` vs. `remote` module, is motivated by 20 | * https://github.com/atom/electron/blob/master/docs/api/remote.md#passing-callbacks-to-the-main-process. 21 | * 22 | * @param {BrowserWindow} window - The window whose events to proxy. 23 | */ 24 | var proxyWindowEvents = function(window) { 25 | var eventsObserved = {}; 26 | 27 | ipc.on('observe-window-event', function(event, arg) { 28 | if ((event.sender === window.webContents) && !eventsObserved[arg]) { 29 | eventsObserved[arg] = function() { 30 | window.webContents.send(arg); 31 | }; 32 | window.on(arg, eventsObserved[arg]); 33 | } 34 | }); 35 | 36 | // Clear our listeners when the page starts (re)loading i.e. its listeners have been purged. 37 | // TODO(wearhere): I'm not sure this is the right event for reload but it seems to work. 38 | window.webContents.on('did-start-loading', function() { 39 | _.each(eventsObserved, function(listener, event) { 40 | window.removeListener(event, listener); 41 | }); 42 | eventsObserved = {}; 43 | }); 44 | }; 45 | 46 | module.exports = proxyWindowEvents; 47 | -------------------------------------------------------------------------------- /client/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // If you are using SublimeLinter, after modifying this config file, be sure to close and reopen 3 | // the file(s) you were editing to see the changes take effect. 4 | 5 | "extends": "../.jshintrc", 6 | 7 | // Whitelist the browser's global variables so JSHint doesn't complain about them being undefined. 8 | "browser": true, 9 | 10 | // Whitelist additional global variables. 11 | "globals": { 12 | // Globals set by `preload.js`. 13 | "ElectronImplementation": false, 14 | 15 | // Meteor's globals. 16 | "Meteor": false, 17 | 18 | // Our dependencies. 19 | "_": false, 20 | 21 | // Our exports. 22 | "Electron": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines useful client-side functionality. 3 | */ 4 | Electron = { 5 | /** 6 | * @return {Boolean} `true` if the app is running in Electron, `false` otherwise. 7 | */ 8 | isDesktop: function(){ 9 | return /Electron/.test(navigator.userAgent); 10 | }, 11 | 12 | /** 13 | * @return {Boolean} `true` if the app is running in Windows, `false` otherwise. 14 | */ 15 | isWindows: function(){ 16 | return /Windows NT/.test(navigator.userAgent); 17 | }, 18 | 19 | 20 | 21 | // When the app is running in Electron, the following methods will be implemented by `preload.js`. 22 | // Stub them out in case the client tries to call them even when not running in Electron. 23 | 24 | /** 25 | * Open the given external protocol URL in the desktop's default manner. (For example, http(s): 26 | * URLs in the user's default browser.) 27 | * 28 | * @param {String} url - The URL to open. 29 | */ 30 | openExternal: function() {}, 31 | 32 | /** 33 | * Determines if the browser window is currently in fullscreen mode. 34 | * 35 | * "Fullscreen" here refers to the state triggered by toggling the native controls, not that 36 | * toggled by the HTML API. 37 | * 38 | * To detect when the browser window changes fullscreen state, observe the 'enter-full-screen' 39 | * and 'leave-full-screen' events using `onWindowEvent`. 40 | * 41 | * @return {Boolean} `true` if the browser window is in fullscreen mode, `false` otherwise. 42 | */ 43 | isFullScreen: function() {}, 44 | 45 | /** 46 | * Invokes _callback_ when the specified `BrowserWindow` event is fired. 47 | * 48 | * See https://github.com/atom/electron/blob/master/docs/api/browser-window.md#events for a list 49 | * of events. 50 | * 51 | * @param {String} event - The name of a `BrowserWindow` event. 52 | * @param {Function} callback - A function to invoke when `event` is triggered. Takes no arguments 53 | * and returns no value. 54 | */ 55 | onWindowEvent: function() {} 56 | }; 57 | 58 | // Read `ElectronImplementation` from the window vs. doing `typeof ElectronImplementation` because 59 | // Meteor will shadow it with a local variable in the latter case. 60 | if (!_.isUndefined(window.ElectronImplementation)) { 61 | // The app is running in Electron. Merge the implementations from `preload.js`. 62 | _.extend(Electron, window.ElectronImplementation); 63 | } 64 | -------------------------------------------------------------------------------- /docs/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-webapps/meteor-electron/5e2faaed015c7a8b14d1d25bde259a736cb5de74/docs/overview.png -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | /* global Package:false, Npm:false */ 2 | 3 | Package.describe({ 4 | name: 'meson:electron', 5 | summary: "Electron", 6 | version: "0.1.4", 7 | git: "https://github.com/electron-webapps/meteor-electron" 8 | }); 9 | 10 | Npm.depends({ 11 | "electron-packager": "https://github.com/mixmaxhq/electron-packager/archive/f511e2680efa39c014d8bedca872168e585f8daf.tar.gz", 12 | "is-running": "1.0.5", 13 | "lucy-dirsum": "https://github.com/mixmaxhq/lucy-dirsum/archive/08299b483cd0f79d18cd0fa1c5081dcab67c5649.tar.gz", 14 | "mkdirp": "0.5.1", 15 | "ncp": "2.0.0", 16 | "rimraf": "2.4.4", 17 | "semver": "5.1.0", 18 | "url-join": "0.0.1", 19 | "electron-rebuild": "1.0.1" 20 | }); 21 | 22 | Package.onUse(function (api) { 23 | api.versionsFrom("METEOR@1.0"); 24 | api.use(["mongo-livedata", "webapp", "ejson", "promise@0.6.7"], "server"); 25 | api.use("underscore", ["server", "client"]); 26 | api.use(["iron:router@0.9.4||1.0.0"], {weak: true}); 27 | api.use("meteorhacks:picker@1.0.0", "server", {weak: true}); 28 | 29 | api.addFiles([ 30 | 'server/createBinaries.js', 31 | 'server/downloadUrls.js', 32 | 'server/launchApp.js', 33 | 'server/serve.js', 34 | 'server/serveDownloadUrl.js', 35 | 'server/serveUpdateFeed.js', 36 | // Must go last so that its dependencies have been defined. 37 | 'server/index.js' 38 | ], 'server'); 39 | 40 | var assets = [ 41 | "app/autoUpdater.js", 42 | "app/main.js", 43 | "app/menu.js", 44 | "app/package.json", 45 | "app/preload.js", 46 | "app/proxyWindowEvents.js" 47 | ]; 48 | 49 | // Use Meteor 1.2+ API, but fall back to the pre-1.2 API if necessary 50 | if (api.addAssets) { 51 | api.addAssets(assets, "server"); 52 | } else { 53 | api.addFiles(assets, "server", {isAsset: true}); 54 | } 55 | 56 | api.addFiles(['client/index.js'], "client"); 57 | 58 | // Test exports. 59 | api.export([ 60 | 'parseMacDownloadUrl', 61 | 'parseWindowsDownloadUrls' 62 | ], 'server', { 63 | testOnly: true 64 | }); 65 | 66 | // Public exports. 67 | api.export("Electron", ["client"]); 68 | }); 69 | 70 | Package.onTest(function(api) { 71 | api.use(['meson:electron', 'tinytest']); 72 | 73 | api.addFiles('tests/server/downloadUrlsTest.js', 'server'); 74 | }); 75 | -------------------------------------------------------------------------------- /server/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // If you are using SublimeLinter, after modifying this config file, be sure to close and reopen 3 | // the file(s) you were editing to see the changes take effect. 4 | 5 | "extends": "../.jshintrc", 6 | 7 | // Whitelist Node's global variables so JSHint doesn't complain about them being undefined. 8 | "node": true, 9 | 10 | // Whitelist additional global variables. 11 | "globals": { 12 | // Meteor's globals. 13 | "Assets": false, 14 | "Meteor": false, 15 | "Npm": false, 16 | "Package": false, 17 | "Promise": false, 18 | 19 | // Our dependencies. 20 | "_": false, 21 | "Mongo": false, 22 | "WebApp": false, 23 | 24 | // Global functions. 25 | "canServeUpdates": true, 26 | "createBinaries": true, 27 | "launchApp": true, 28 | "parseMacDownloadUrl": true, 29 | "parseWindowsDownloadUrls": true, 30 | "serve": true, 31 | "serveDir": true, 32 | "serveDownloadUrl": true, 33 | "serveUpdateFeed": true, 34 | 35 | // Global variables. 36 | "DOWNLOAD_URLS": true, 37 | "UPDATE_FEED_PATH": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/createBinaries.js: -------------------------------------------------------------------------------- 1 | var electronPackager = Meteor.wrapAsync(Npm.require("electron-packager")); 2 | var electronRebuild = Npm.require('electron-rebuild'); 3 | var fs = Npm.require('fs'); 4 | var mkdirp = Meteor.wrapAsync(Npm.require('mkdirp')); 5 | var path = Npm.require('path'); 6 | var proc = Npm.require('child_process'); 7 | var dirsum = Meteor.wrapAsync(Npm.require('lucy-dirsum')); 8 | var readFile = Meteor.wrapAsync(fs.readFile); 9 | var writeFile = Meteor.wrapAsync(fs.writeFile); 10 | var stat = Meteor.wrapAsync(fs.stat); 11 | var util = Npm.require('util'); 12 | var rimraf = Meteor.wrapAsync(Npm.require('rimraf')); 13 | var ncp = Meteor.wrapAsync(Npm.require('ncp')); 14 | 15 | var exec = Meteor.wrapAsync(function(command, options, callback){ 16 | proc.exec(command, options, function(err, stdout, stderr){ 17 | callback(err, {stdout: stdout, stderr: stderr}); 18 | }); 19 | }); 20 | 21 | var exists = function(path) { 22 | try { 23 | stat(path); 24 | return true; 25 | } catch(e) { 26 | return false; 27 | } 28 | }; 29 | 30 | var projectRoot = function(){ 31 | if (process.platform === "win32"){ 32 | return process.env.METEOR_SHELL_DIR.split(".meteor")[0]; 33 | } else { 34 | return process.env.PWD; 35 | } 36 | }; 37 | 38 | var ELECTRON_VERSION = '0.36.7'; 39 | 40 | var electronSettings = Meteor.settings.electron || {}; 41 | 42 | var IS_MAC = (process.platform === 'darwin'); 43 | 44 | /* Entry Point */ 45 | createBinaries = function() { 46 | var results = {}; 47 | var builds; 48 | if (electronSettings.builds){ 49 | builds = electronSettings.builds; 50 | } else { 51 | //just build for the current platform/architecture 52 | if (process.platform === "darwin"){ 53 | builds = [{platform: process.platform, arch: process.arch}]; 54 | } else if (process.platform === "win32"){ 55 | //arch detection doesn't always work on windows, and ia32 works everywhere 56 | builds = [{platform: process.platform, arch: "ia32"}]; 57 | } else { 58 | console.error('You must specify one or more builds in Meteor.settings.electron.'); 59 | return results; 60 | } 61 | } 62 | 63 | if (_.isEmpty(builds)) { 64 | console.error('No builds available for this platform.'); 65 | return results; 66 | } 67 | 68 | builds.forEach(function(buildInfo){ 69 | var buildRequired = false; 70 | 71 | var buildDirs = createBuildDirectories(buildInfo); 72 | 73 | /* Write out Electron application files */ 74 | var appVersion = electronSettings.version; 75 | var appName = electronSettings.name || "electron"; 76 | var appDescription = electronSettings.description; 77 | 78 | var resolvedAppSrcDir; 79 | if (electronSettings.appSrcDir) { 80 | resolvedAppSrcDir = path.join(projectRoot(), electronSettings.appSrcDir); 81 | } else { 82 | // See http://stackoverflow.com/a/29745318/495611 for how the package asset directory is derived. 83 | // We can't read this from the project directory like the user-specified app directory since 84 | // we may be loaded from Atmosphere rather than locally. 85 | resolvedAppSrcDir = path.join(process.cwd(), 'assets', 'packages', 'meson_electron', 'app'); 86 | } 87 | 88 | // Check if the package.json has changed before copying over the app files, to account for 89 | // changes made in the app source dir. 90 | var packagePath = packageJSONPath(resolvedAppSrcDir); 91 | var packageJSON = Npm.require(packagePath); 92 | 93 | // Fill in missing package.json fields (note: before the comparison). 94 | // This isn't just a convenience--`Squirrel.Windows` requires the description and version. 95 | packageJSON = _.defaults(packageJSON, { 96 | name: appName && appName.toLowerCase().replace(/\s/g, '-'), 97 | productName: appName, 98 | description: appDescription, 99 | version: appVersion 100 | }); 101 | // Check if the package has changed before we possibly copy over the app source since that will 102 | // of course sync `package.json`. 103 | var packageHasChanged = packageJSONHasChanged(packageJSON, buildDirs.app); 104 | 105 | var didOverwriteNodeModules = false; 106 | 107 | if (appHasChanged(resolvedAppSrcDir, buildDirs.working)) { 108 | buildRequired = true; 109 | 110 | // Copy the app directory over while also pruning old files. 111 | if (IS_MAC) { 112 | // Ensure that the app source directory ends in a slash so we copy its contents. 113 | // Except node_modules from pruning since we prune that below. 114 | // TODO(wearhere): `rsync` also uses checksums to only copy what's necessary so theoretically we 115 | // could always `rsync` rather than checking if the directory's changed first. 116 | exec(util.format('rsync -a --delete --force --filter="P node_modules" "%s" "%s"', 117 | path.join(resolvedAppSrcDir, '/'), buildDirs.app)); 118 | } else { 119 | // TODO(wearhere): More efficient sync on Windows (where `rsync` isn't available.) 120 | rimraf(buildDirs.app); 121 | mkdirp(buildDirs.app); 122 | ncp(resolvedAppSrcDir, buildDirs.app); 123 | didOverwriteNodeModules = true; 124 | } 125 | } 126 | 127 | /* Write out the application package.json */ 128 | // Do this after writing out the application files, since that will overwrite `package.json`. 129 | // This logic is a little bit inefficient: it's not the case that _every_ change to package.json 130 | // means that we have to reinstall the node modules; and if we overwrote the node modules, we 131 | // don't necessarily have to rewrite `package.json`. But doing it altogether is simplest. 132 | if (packageHasChanged || didOverwriteNodeModules) { 133 | buildRequired = true; 134 | 135 | // For some reason when this file isn't manually removed it fails to be overwritten with an 136 | // EACCES error. 137 | rimraf(packageJSONPath(buildDirs.app)); 138 | writeFile(packageJSONPath(buildDirs.app), JSON.stringify(packageJSON)); 139 | 140 | exec("npm install && npm prune", {cwd: buildDirs.app}); 141 | 142 | // Rebuild native modules if any. 143 | // TODO(jeff): Start using the pre-gyp fix if someone asks for it, so we can make sure it works: 144 | // https://github.com/electronjs/electron-rebuild#node-pre-gyp-workaround 145 | Promise.await(electronRebuild.installNodeHeaders(ELECTRON_VERSION, null /* nodeDistUrl */, 146 | null /* headersDir */, buildInfo.arch)); 147 | Promise.await(electronRebuild.rebuildNativeModules(ELECTRON_VERSION, 148 | path.join(buildDirs.app, 'node_modules'), null /* headersDir */, buildInfo.arch)); 149 | } 150 | 151 | /* Write out Electron Settings */ 152 | var settings = _.defaults({}, electronSettings, { 153 | rootUrl: process.env.ROOT_URL 154 | }); 155 | 156 | var signingIdentity = electronSettings.sign; 157 | var signingIdentityRequiredAndMissing = false; 158 | if (canServeUpdates(buildInfo.platform)) { 159 | // Enable the auto-updater if possible. 160 | if ((buildInfo.platform === 'darwin') && !signingIdentity) { 161 | // If the app isn't signed and we try to use the auto-updater, it will 162 | // throw an exception. Log an error if the settings have changed, below. 163 | signingIdentityRequiredAndMissing = true; 164 | } else { 165 | settings.updateFeedUrl = settings.rootUrl + UPDATE_FEED_PATH; 166 | } 167 | } 168 | 169 | if (settingsHaveChanged(settings, buildDirs.app)) { 170 | if (signingIdentityRequiredAndMissing) { 171 | console.error('Developer ID signing identity is missing: remote updates will not work.'); 172 | } 173 | buildRequired = true; 174 | writeFile(settingsPath(buildDirs.app), JSON.stringify(settings)); 175 | } 176 | 177 | var packagerSettings = getPackagerSettings(buildInfo, buildDirs); 178 | if (packagerSettings.icon && iconHasChanged(packagerSettings.icon, buildDirs.working)) { 179 | buildRequired = true; 180 | } 181 | 182 | // TODO(wearhere): If/when the signing identity expires, does its name change? If not, we'll need 183 | // to force the app to be rebuilt somehow. 184 | 185 | if (packagerSettingsHaveChanged(packagerSettings, buildDirs.working)) { 186 | buildRequired = true; 187 | } 188 | 189 | var app = appPath(appName, buildInfo.platform, buildInfo.arch, buildDirs.build); 190 | if (!exists(app)) { 191 | buildRequired = true; 192 | } 193 | 194 | /* Create Build */ 195 | if (buildRequired) { 196 | var build = electronPackager(packagerSettings)[0]; 197 | console.log("Build created for ", buildInfo.platform, buildInfo.arch, "at", build); 198 | } 199 | 200 | /* Package the build for download if specified. */ 201 | // TODO(rissem): make this platform independent 202 | 203 | if (electronSettings.autoPackage && (buildInfo.platform === 'darwin')) { 204 | // The auto-updater framework only supports installing ZIP releases: 205 | // https://github.com/Squirrel/Squirrel.Mac#update-json-format 206 | var downloadName = (appName || "app") + ".zip"; 207 | var compressedDownload = path.join(buildDirs.final, downloadName); 208 | 209 | if (buildRequired || !exists(compressedDownload)) { 210 | // Use `ditto` to ZIP the app because I couldn't find a good npm module to do it and also that's 211 | // what a couple of other related projects do: 212 | // - https://github.com/Squirrel/Squirrel.Mac/blob/8caa2fa2007b29a253f7f5be8fc9f36ace6aa30e/Squirrel/SQRLZipArchiver.h#L24 213 | // - https://github.com/jenslind/electron-release/blob/4a2a701c18664ec668c3570c3907c0fee72f5e2a/index.js#L109 214 | exec('ditto -ck --sequesterRsrc --keepParent "' + app + '" "' + compressedDownload + '"'); 215 | console.log("Downloadable created at", compressedDownload); 216 | } 217 | } 218 | 219 | results[buildInfo.platform + "-" + buildInfo.arch] = { 220 | app: app, 221 | buildRequired: buildRequired 222 | }; 223 | }); 224 | 225 | return results; 226 | }; 227 | 228 | function createBuildDirectories(build){ 229 | // Use a predictable directory so that other scripts can locate the builds, also so that the builds 230 | // may be cached: 231 | 232 | var workingDir = path.join(projectRoot(), '.meteor-electron', build.platform + "-" + build.arch); 233 | mkdirp(workingDir); 234 | 235 | //TODO consider seeding the binaryDir from package assets so package 236 | //could work without an internet connection 237 | 238 | // *binaryDir* holds the vanilla electron apps 239 | var binaryDir = path.join(workingDir, "releases"); 240 | mkdirp(binaryDir); 241 | 242 | // *appDir* holds the electron application that points to a meteor app 243 | var appDir = path.join(workingDir, "apps"); 244 | mkdirp(appDir); 245 | 246 | // *buildDir* contains the uncompressed apps 247 | var buildDir = path.join(workingDir, "builds"); 248 | mkdirp(buildDir); 249 | 250 | // *finalDir* contains zipped apps ready to be downloaded 251 | var finalDir = path.join(workingDir, "final"); 252 | mkdirp(finalDir); 253 | 254 | return { 255 | working: workingDir, 256 | binary: binaryDir, 257 | app: appDir, 258 | build: buildDir, 259 | final: finalDir 260 | }; 261 | } 262 | 263 | function getPackagerSettings(buildInfo, dirs){ 264 | var packagerSettings = { 265 | dir: dirs.app, 266 | name: electronSettings.name || "Electron", 267 | platform: buildInfo.platform, 268 | arch: buildInfo.arch, 269 | version: ELECTRON_VERSION, 270 | out: dirs.build, 271 | cache: dirs.binary, 272 | overwrite: true, 273 | // The EXE's `ProductName` is the preferred title of application shortcuts created by `Squirrel.Windows`. 274 | // If we don't set it, it will default to "Electron". 275 | 'version-string': { 276 | ProductName: electronSettings.name || 'Electron' 277 | } 278 | }; 279 | 280 | if (electronSettings.version) { 281 | packagerSettings['app-version'] = electronSettings.version; 282 | } 283 | if (electronSettings.icon) { 284 | var icon = electronSettings.icon[buildInfo.platform]; 285 | if (icon) { 286 | var iconPath = path.join(projectRoot(), icon); 287 | packagerSettings.icon = iconPath; 288 | } 289 | } 290 | if (electronSettings.sign) { 291 | packagerSettings.sign = electronSettings.sign; 292 | } 293 | if (electronSettings.protocols) { 294 | packagerSettings.protocols = electronSettings.protocols; 295 | } 296 | return packagerSettings; 297 | } 298 | 299 | function settingsPath(appDir) { 300 | return path.join(appDir, 'electronSettings.json'); 301 | } 302 | 303 | function settingsHaveChanged(settings, appDir) { 304 | var electronSettingsPath = settingsPath(appDir); 305 | var existingElectronSettings; 306 | try { 307 | existingElectronSettings = Npm.require(electronSettingsPath); 308 | } catch(e) { 309 | // No existing settings. 310 | } 311 | return !existingElectronSettings || !_.isEqual(settings, existingElectronSettings); 312 | } 313 | 314 | function appHasChanged(appSrcDir, workingDir) { 315 | var appChecksumPath = path.join(workingDir, 'appChecksum.txt'); 316 | var existingAppChecksum; 317 | try { 318 | existingAppChecksum = readFile(appChecksumPath, 'utf8'); 319 | } catch(e) { 320 | // No existing checksum. 321 | } 322 | 323 | var appChecksum = dirsum(appSrcDir); 324 | if (appChecksum !== existingAppChecksum) { 325 | writeFile(appChecksumPath, appChecksum); 326 | return true; 327 | } else { 328 | return false; 329 | } 330 | } 331 | 332 | function packageJSONPath(appDir) { 333 | return path.join(appDir, 'package.json'); 334 | } 335 | 336 | function packageJSONHasChanged(packageJSON, appDir) { 337 | var packagePath = packageJSONPath(appDir); 338 | var existingPackageJSON; 339 | try { 340 | existingPackageJSON = Npm.require(packagePath); 341 | } catch(e) { 342 | // No existing package. 343 | } 344 | 345 | return !existingPackageJSON || !_.isEqual(packageJSON, existingPackageJSON); 346 | } 347 | 348 | function packagerSettingsHaveChanged(settings, workingDir) { 349 | var settingsPath = path.join(workingDir, 'lastUsedPackagerSettings.json'); 350 | var existingPackagerSettings; 351 | try { 352 | existingPackagerSettings = Npm.require(settingsPath); 353 | } catch(e) { 354 | // No existing settings. 355 | } 356 | 357 | if (!existingPackagerSettings || !_.isEqual(settings, existingPackagerSettings)) { 358 | writeFile(settingsPath, JSON.stringify(settings)); 359 | return true; 360 | } else { 361 | return false; 362 | } 363 | } 364 | 365 | function iconHasChanged(iconPath, workingDir) { 366 | var iconChecksumPath = path.join(workingDir, 'iconChecksum.txt'); 367 | var existingIconChecksum; 368 | try { 369 | existingIconChecksum = readFile(iconChecksumPath, 'utf8'); 370 | } catch(e) { 371 | // No existing checksum. 372 | } 373 | 374 | // `dirsum` works for files too. 375 | var iconChecksum = dirsum(iconPath); 376 | if (iconChecksum !== existingIconChecksum) { 377 | writeFile(iconChecksumPath, iconChecksum); 378 | return true; 379 | } else { 380 | return false; 381 | } 382 | } 383 | 384 | function appPath(appName, platform, arch, buildDir) { 385 | var appExtension = (platform === 'darwin') ? '.app' : '.exe'; 386 | return path.join(buildDir, [appName, platform, arch].join('-'), appName + appExtension); 387 | } 388 | -------------------------------------------------------------------------------- /server/downloadUrls.js: -------------------------------------------------------------------------------- 1 | var urlJoin = Npm.require('url-join'); 2 | 3 | // Global for tests. 4 | parseMacDownloadUrl = function(electronSettings) { 5 | if (!electronSettings || !electronSettings.downloadUrls || !electronSettings.downloadUrls.darwin) return; 6 | 7 | return electronSettings.downloadUrls.darwin.replace('{{version}}', electronSettings.version); 8 | }; 9 | 10 | // Global for tests. 11 | parseWindowsDownloadUrls = function(electronSettings) { 12 | if (!electronSettings || !electronSettings.downloadUrls || !electronSettings.downloadUrls.win32) return; 13 | 14 | // The default value here is what `createBinaries` writes into the app's package.json, which is 15 | // what is read by `grunt-electron-installer` to name the installer. 16 | var appName = electronSettings.name || 'electron'; 17 | 18 | var releasesUrl, installerUrl; 19 | var installerUrlIsVersioned = false; 20 | 21 | if (_.isString(electronSettings.downloadUrls.win32)) { 22 | if (electronSettings.downloadUrls.win32.indexOf('{{version}}') > -1) { 23 | console.error('Only the Windows installer URL may be versioned. Specify `downloadUrls.win32.installer`.'); 24 | return; 25 | } 26 | releasesUrl = electronSettings.downloadUrls.win32; 27 | // 'AppSetup.exe' refers to the output of `grunt-electron-installer`. 28 | installerUrl = urlJoin(electronSettings.downloadUrls.win32, appName + 'Setup.exe'); 29 | } else { 30 | releasesUrl = electronSettings.downloadUrls.win32.releases; 31 | if (releasesUrl.indexOf('{{version}}') > -1) { 32 | console.error('Only the Windows installer URL may be versioned.'); 33 | return; 34 | } 35 | installerUrl = electronSettings.downloadUrls.win32.installer; 36 | if (installerUrl.indexOf('{{version}}') > -1) { 37 | installerUrl = installerUrl.replace('{{version}}', electronSettings.version); 38 | installerUrlIsVersioned = true; 39 | } 40 | } 41 | 42 | // Cachebust the installer URL if it's not versioned. 43 | // (The releases URL will also be cachebusted, but by `serveUpdateFeed` since we've got to append 44 | // the particular paths requested by the client). 45 | if (!installerUrlIsVersioned) { 46 | installerUrl = cachebustedUrl(installerUrl); 47 | } 48 | 49 | return { 50 | releases: releasesUrl, 51 | installer: installerUrl 52 | }; 53 | }; 54 | 55 | function cachebustedUrl(url) { 56 | var querySeparator = (url.indexOf('?') > -1) ? '&' : '?'; 57 | return url + querySeparator + 'cb=' + Date.now(); 58 | } 59 | 60 | DOWNLOAD_URLS = { 61 | darwin: parseMacDownloadUrl(Meteor.settings.electron), 62 | win32: parseWindowsDownloadUrls(Meteor.settings.electron) 63 | }; 64 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | var electronSettings = Meteor.settings.electron || {}; 2 | 3 | if ((process.env.NODE_ENV === 'development') && (electronSettings.autoBuild !== false)) { 4 | var buildResults = createBinaries(); 5 | var buildResultForThisPlatform = buildResults[process.platform + '-' + process.arch]; 6 | if (buildResultForThisPlatform) { 7 | launchApp(buildResultForThisPlatform.app, buildResultForThisPlatform.buildRequired); 8 | } 9 | } 10 | 11 | serveDownloadUrl(); 12 | serveUpdateFeed(); 13 | -------------------------------------------------------------------------------- /server/launchApp.js: -------------------------------------------------------------------------------- 1 | var isRunning = Meteor.wrapAsync(Npm.require("is-running")); 2 | var path = Npm.require('path'); 3 | var proc = Npm.require('child_process'); 4 | 5 | var ElectronProcesses = new Mongo.Collection("processes"); 6 | 7 | var ProcessManager = { 8 | add: function(pid){ 9 | ElectronProcesses.insert({ pid: pid }); 10 | }, 11 | 12 | running: function(){ 13 | var runningProcess; 14 | ElectronProcesses.find().forEach(function(proc){ 15 | if (isRunning(proc.pid)){ 16 | runningProcess = proc.pid; 17 | } else { 18 | ElectronProcesses.remove({ _id: proc._id }); 19 | } 20 | }); 21 | return runningProcess; 22 | }, 23 | 24 | stop: function(pid) { 25 | process.kill(pid); 26 | ElectronProcesses.remove({ pid: pid }); 27 | } 28 | }; 29 | 30 | launchApp = function(app, appIsNew) { 31 | // Safeguard. 32 | if (process.env.NODE_ENV !== 'development') return; 33 | 34 | var runningProcess = ProcessManager.running(); 35 | if (runningProcess) { 36 | if (!appIsNew) { 37 | return; 38 | } else { 39 | ProcessManager.stop(runningProcess); 40 | } 41 | } 42 | 43 | var electronExecutable, child; 44 | if (process.platform === 'win32') { 45 | electronExecutable = app; 46 | child = proc.spawn(electronExecutable); 47 | } else { 48 | electronExecutable = path.join(app, "Contents", "MacOS", "Electron"); 49 | var appDir = path.join(app, "Contents", "Resources", "app"); 50 | 51 | //TODO figure out how to handle case where electron executable or 52 | //app dir don't exist 53 | 54 | child = proc.spawn(electronExecutable, [appDir]); 55 | } 56 | 57 | child.stdout.on("data", function(data){ 58 | console.log("ATOM:", data.toString()); 59 | }); 60 | 61 | child.stderr.on("data", function(data){ 62 | console.log("ATOM:", data.toString()); 63 | }); 64 | 65 | ProcessManager.add(child.pid); 66 | }; 67 | -------------------------------------------------------------------------------- /server/serve.js: -------------------------------------------------------------------------------- 1 | serve = function(path, handler) { 2 | if (Package["iron:router"]){ 3 | Package["iron:router"].Router.route(path, function(){ 4 | handler(this.request, this.response, this.next); 5 | }, {where: "server"}); 6 | } else if (Package["meteorhacks:picker"]){ 7 | Package["meteorhacks:picker"].Picker.route(path, function(params, req, res, next){ 8 | req.query = params.query; 9 | handler(req, res, next); 10 | }); 11 | } else { 12 | WebApp.rawConnectHandlers.use(function(req, res, next){ 13 | if (req.path === path) { 14 | handler(req, res, next); 15 | } else { 16 | next(); 17 | } 18 | }); 19 | } 20 | }; 21 | 22 | serveDir = function(dir, handler){ 23 | //path starts with dir 24 | if (Package["iron:router"]){ 25 | Package["iron:router"].Router.route(dir + "/:stuff", function(){ 26 | handler(this.request, this.response, this.next); 27 | }, {where: "server"}); 28 | } else if (Package["meteorhacks:picker"]){ 29 | Package["meteorhacks:picker"].Picker.route(dir + "/:stuff", function(params, req, res, next){ 30 | req.query = params.query; 31 | handler(req, res, next); 32 | }); 33 | } else { 34 | var regex = new RegExp("^" + dir); 35 | WebApp.rawConnectHandlers.use(function(req, res, next){ 36 | if (regex.test(req.path)) { 37 | handler(req, res, next); 38 | } else { 39 | next(); 40 | } 41 | }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /server/serveDownloadUrl.js: -------------------------------------------------------------------------------- 1 | serveDownloadUrl = function() { 2 | serve('/app/download', function(req, res, next) { 3 | var installerUrl = DOWNLOAD_URLS[req.query.platform]; 4 | if (_.isObject(installerUrl)) { 5 | installerUrl = installerUrl.installer; 6 | } 7 | if (installerUrl) { 8 | res.statusCode = 302; // Moved Temporarily 9 | res.setHeader('Location', installerUrl); 10 | res.end(); 11 | } else { 12 | res.statusCode = 404; 13 | res.end(); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /server/serveUpdateFeed.js: -------------------------------------------------------------------------------- 1 | var semver = Npm.require('semver'); 2 | var urlJoin = Npm.require('url-join'); 3 | var electronSettings = Meteor.settings.electron || {}; 4 | var latestVersion = electronSettings.version; 5 | 6 | canServeUpdates = function(platform) { 7 | if (!latestVersion){ 8 | return false; 9 | } 10 | 11 | return !!DOWNLOAD_URLS[platform]; 12 | }; 13 | 14 | UPDATE_FEED_PATH = "/app/latest"; 15 | 16 | serveUpdateFeed = function() { 17 | // https://github.com/Squirrel/Squirrel.Mac#server-support 18 | if (canServeUpdates("darwin")){ 19 | serve(UPDATE_FEED_PATH, function(req, res, next) { 20 | var appVersion = req.query.version; 21 | if (semver.valid(appVersion) && semver.gte(appVersion, latestVersion)) { 22 | res.statusCode = 204; // No content. 23 | res.end(); 24 | } else { 25 | res.statusCode = 200; 26 | res.setHeader('Content-Type', 'application/json'); 27 | res.end(JSON.stringify({ 28 | url: DOWNLOAD_URLS['darwin'] 29 | })); 30 | } 31 | }); 32 | } 33 | 34 | // https://github.com/squirrel/squirrel.windows 35 | // (Summary 'cause those docs are scant: the Windows app is going to expect the update feed URL 36 | // to represent a directory from within which it can fetch the RELEASES file and packages. The 37 | // above `serve` call serves _just_ '/app/latest', whereas this serves its contents.) 38 | if (canServeUpdates("win32")) { 39 | // `path.dirname` works even on Windows. 40 | var releasesUrl = DOWNLOAD_URLS['win32'].releases; 41 | serveDir(UPDATE_FEED_PATH, function(req, res, next){ 42 | //first strip off the UPDATE_FEED_PATH 43 | var path = req.url.split(UPDATE_FEED_PATH)[1]; 44 | res.statusCode = 302; 45 | // Cache-bust the RELEASES file. 46 | if (/RELEASES/.test(path)) { 47 | path += (/\?/.test(path) ? '&' : '?') + 'cb=' + Date.now(); 48 | } 49 | res.setHeader("Location", urlJoin(releasesUrl, path)); 50 | res.end(); 51 | }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /tests/server/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // If you are using SublimeLinter, after modifying this config file, be sure to close and reopen 3 | // the file(s) you were editing to see the changes take effect. 4 | 5 | "extends": "../../server/.jshintrc", 6 | 7 | // Whitelist additional global variables. 8 | "globals": { 9 | // Meteor's globals. 10 | "Tinytest": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/server/downloadUrlsTest.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************/ 2 | /* Mac */ 3 | /******************************************************************************/ 4 | 5 | Tinytest.add('download URL parsing - Mac - returns undefined if `downloadUrls.darwin` not specified', function(test) { 6 | test.isUndefined(parseMacDownloadUrl()); 7 | test.isUndefined(parseMacDownloadUrl({ 8 | downloadUrls: {} 9 | })); 10 | }); 11 | 12 | Tinytest.add('download URL parsing - Mac - returns the specified URL', function(test) { 13 | test.equal(parseMacDownloadUrl({ 14 | downloadUrls: { 15 | darwin: 'https://myapp.com/download/osx/MyApp.zip' 16 | } 17 | }), 'https://myapp.com/download/osx/MyApp.zip'); 18 | }); 19 | 20 | Tinytest.add('download URL parsing - Mac - versions the specified URL', function(test) { 21 | test.equal(parseMacDownloadUrl({ 22 | version: '1.1.1', 23 | downloadUrls: { 24 | darwin: 'https://myapp.com/download/osx/{{version}}/MyApp.zip' 25 | } 26 | }), 'https://myapp.com/download/osx/1.1.1/MyApp.zip'); 27 | }); 28 | 29 | /******************************************************************************/ 30 | /* Windows */ 31 | /******************************************************************************/ 32 | 33 | /* 34 | * a unified URL 35 | */ 36 | Tinytest.add('download URL parsing - Windows - a unified URL - returns undefined if `downloadUrls.win32` not specified', function(test) { 37 | test.isUndefined(parseWindowsDownloadUrls()); 38 | test.isUndefined(parseWindowsDownloadUrls({ 39 | downloadUrls: {} 40 | })); 41 | }); 42 | 43 | Tinytest.add('download URL parsing - Windows - a unified URL - attempting to version fails', function(test) { 44 | var origConsoleErr = console.error; 45 | var consoleErrorWasCalled = false; 46 | console.error = function() { consoleErrorWasCalled = true; }; 47 | 48 | try { 49 | test.isUndefined(parseWindowsDownloadUrls({ 50 | downloadUrls: { 51 | win32: 'https://myapp.com/download/win32/{{version}}' 52 | } 53 | })); 54 | test.isTrue(consoleErrorWasCalled); 55 | } finally { 56 | console.error = origConsoleErr; 57 | } 58 | }); 59 | 60 | /* the releases URL */ 61 | Tinytest.add('download URL parsing - Windows - a unified URL - the releases URL - is returned properly', function(test) { 62 | var releasesUrl = parseWindowsDownloadUrls({ 63 | downloadUrls: { 64 | win32: 'https://myapp.com/download/win32' 65 | } 66 | }).releases; 67 | 68 | test.equal(releasesUrl, 'https://myapp.com/download/win32'); 69 | }); 70 | 71 | /* the installer URL */ 72 | Tinytest.add('download URL parsing - Windows - a unified URL - the installer URL - is returned properly', function(test) { 73 | var installerUrl = parseWindowsDownloadUrls({ 74 | name: 'MyApp', 75 | downloadUrls: { 76 | win32: 'https://myapp.com/download/win32' 77 | } 78 | }).installer; 79 | 80 | // Not `equal` because of cachebusting (tested next). 81 | test.isTrue(installerUrl && (installerUrl.indexOf('https://myapp.com/download/win32/MyAppSetup.exe') === 0)); 82 | }); 83 | 84 | Tinytest.add('download URL parsing - Windows - a unified URL - the installer URL - is cachebusted', function(test) { 85 | var installerUrl = parseWindowsDownloadUrls({ 86 | name: 'MyApp', 87 | downloadUrls: { 88 | win32: 'https://myapp.com/download/win32' 89 | } 90 | }).installer; 91 | 92 | test.isTrue(installerUrl && (installerUrl.indexOf('?cb=') > -1)); 93 | }); 94 | 95 | /* 96 | * separate URLs 97 | */ 98 | 99 | /* the releases URL */ 100 | Tinytest.add('download URL parsing - Windows - separate URLs - the releases URL - attempting to version fails', function(test) { 101 | var origConsoleErr = console.error; 102 | var consoleErrorWasCalled = false; 103 | console.error = function() { consoleErrorWasCalled = true; }; 104 | 105 | try { 106 | test.isUndefined(parseWindowsDownloadUrls({ 107 | downloadUrls: { 108 | win32: { 109 | releases: 'https://myapp.com/download/win32/{{version}}', 110 | installer: 'https://myapp.com/download/win32/{{version}}/MyAppSetup.exe' 111 | } 112 | } 113 | })); 114 | test.isTrue(consoleErrorWasCalled); 115 | } finally { 116 | console.error = origConsoleErr; 117 | } 118 | }); 119 | 120 | Tinytest.add('download URL parsing - Windows - separate URLs - the releases URL - is returned as specified', function(test) { 121 | var releasesUrl = parseWindowsDownloadUrls({ 122 | downloadUrls: { 123 | win32: { 124 | releases: 'https://myapp.com/download/win32', 125 | installer: 'https://myapp.com/download/win32/MyAppSetup.exe' 126 | } 127 | } 128 | }).releases; 129 | 130 | test.equal(releasesUrl, 'https://myapp.com/download/win32'); 131 | }); 132 | 133 | /* the installer URL */ 134 | Tinytest.add('download URL parsing - Windows - separate URLs - the installer URL - is returned as specified', function(test) { 135 | var installerUrl = parseWindowsDownloadUrls({ 136 | downloadUrls: { 137 | win32: { 138 | releases: 'https://myapp.com/download/win32', 139 | installer: 'https://myapp.com/download/win32/MyAppSetup.exe' 140 | } 141 | } 142 | }).installer; 143 | 144 | // Not `equal` because of cachebusting (tested next). 145 | test.isTrue(installerUrl && (installerUrl.indexOf('https://myapp.com/download/win32') === 0)); 146 | }); 147 | 148 | Tinytest.add('download URL parsing - Windows - separate URLs - the installer URL - is cachebusted if not versioned', function(test) { 149 | var installerUrl = parseWindowsDownloadUrls({ 150 | downloadUrls: { 151 | win32: { 152 | releases: 'https://myapp.com/download/win32', 153 | installer: 'https://myapp.com/download/win32/MyAppSetup.exe' 154 | } 155 | } 156 | }).installer; 157 | 158 | test.isTrue(installerUrl && (installerUrl.indexOf('?cb=') > -1)); 159 | }); 160 | 161 | Tinytest.add('download URL parsing - Windows - separate URLs - the installer URL - is versioned as specified (and then not cachebusted)', function(test) { 162 | var installerUrl = parseWindowsDownloadUrls({ 163 | version: '1.1.1', 164 | downloadUrls: { 165 | win32: { 166 | releases: 'https://myapp.com/download/win32', 167 | installer: 'https://myapp.com/download/win32/{{version}}/MyAppSetup.exe' 168 | } 169 | } 170 | }).installer; 171 | 172 | test.equal(installerUrl, 'https://myapp.com/download/win32/1.1.1/MyAppSetup.exe'); 173 | }); 174 | --------------------------------------------------------------------------------