├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc.js ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __mocks__ ├── chalk.js ├── child_process.js ├── chokidar.js ├── commander.js ├── ejs.js ├── electron-builder.js ├── fs-extra.js ├── fs.js ├── inquirer.js ├── latest-version.js ├── merge-config.js └── webpack.js ├── __test__ ├── builder │ └── index.test.js ├── cleaner │ └── index.test.js ├── generator │ ├── index.test.js │ └── questionnaire.test.js ├── index.test.js ├── packager │ └── index.test.js ├── runner │ └── index.test.js ├── setup.js ├── starter │ └── index.test.js ├── test_runner │ └── index.test.js └── utils │ ├── checker.test.js │ └── index.test.js ├── babel.config.js ├── bin └── bozon ├── jest.config.js ├── package.json ├── src ├── __mocks__ │ ├── builder.js │ ├── cleaner.js │ ├── generator.js │ ├── packager.js │ ├── starter.js │ ├── test_runner.js │ └── utils.js ├── builder │ ├── bundle.js │ ├── html.js │ ├── index.js │ ├── manifest.js │ ├── messages.js │ ├── watcher.js │ └── webpack_config │ │ ├── __mocks__ │ │ └── index.js │ │ ├── defaults.js │ │ └── index.js ├── cleaner │ └── index.js ├── dev │ └── index.js ├── generator │ ├── __mocks__ │ │ └── questionnaire.js │ ├── index.js │ └── questionnaire.js ├── index.js ├── packager │ └── index.js ├── runner │ └── index.js ├── starter │ └── index.js ├── test_runner │ └── index.js └── utils │ ├── __mocks__ │ ├── checker.js │ └── logger.js │ ├── checker.js │ ├── index.js │ ├── logger.js │ └── spinner.js ├── templates ├── gitignore ├── images │ ├── electron.icns │ ├── electron.ico │ └── electron.png ├── index.html ├── javascripts │ ├── main.js │ ├── preload.js │ └── renderer.js ├── jest.config.js ├── json │ ├── development.json │ ├── development_package.json │ ├── linux.json │ ├── mac.json │ ├── production.json │ ├── settings.json │ ├── test.json │ └── windows.json ├── license ├── readme.md ├── stylesheets │ └── application.css ├── test │ ├── main_test.js │ └── setup.js └── webpack.config.js ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 |    3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | quote_type = single 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | jest: true 7 | }, 8 | extends: ['standard'], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | __non_webpack_require__: 'readonly', 13 | CONFIG: 'readonly' 14 | }, 15 | parserOptions: { 16 | ecmaVersion: 2021, 17 | sourceType: 'module' 18 | }, 19 | rules: { 20 | semi: 'off', 21 | 'space-before-function-paren': 'off' 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Setup node 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node }} 28 | 29 | - uses: c-hive/gha-yarn-cache@v2 30 | 31 | - name: Install JS dependencies 32 | run: yarn install 33 | 34 | - name: Lint 35 | run: yarn lint 36 | 37 | - name: Test 38 | run: yarn test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | dist 4 | .vscode 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nvmrc 2 | .editorconfig 3 | .eslintrc.js 4 | .prettierrc.js 5 | babel.config.js 6 | .vscode/ 7 | .circleci/ 8 | __test__/ 9 | __mocks__/ 10 | src/ 11 | node_modules/ 12 | CONTRIBUTING.md 13 | yarn-error.log 14 | ./jest.config.js 15 | ./webpack.config.js 16 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.10.0 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'none', 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Bozon 2 | 3 | >First off, thank you for considering contributing to Bozon. Any help, ideas and inputs are really appreciated! 4 | 5 | The following is a set of guidelines for contributing to Bozon. These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. 6 | 7 | ## Submitting Issues 8 | * You can create an issue [here](https://github.com/railsware/bozon/issues), but before doing that please check existing issues and join conversation instead of creating new one if you find topic related to your issue. Otherwise read the notes below and include as many details as possible with your report. If you can, please include: 9 | 10 | * The version of Bozon you are using 11 | * The operating system you are using 12 | * What you were doing when the issue arose and what you expected to happen 13 | 14 | What would be also helpful: 15 | * Screenshots and animated GIFs 16 | * Error output that appears in your terminal 17 | 18 | ## Submitting Pull Requests 19 | 20 | To check that your contributions does not break anything make sure `npm test` passes. 21 | 22 | Please ensure your pull request adheres to the following guidelines: 23 | 24 | * Make an individual pull request for each suggestion 25 | * The pull request should have a useful title and detailed description 26 | * Make sure your text editor is set to remove trailing whitespace. 27 | * Use short, present tense commit messages. 28 | * Use the present tense ("Add feature" not "Added feature") 29 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 30 | * Limit the first line to 72 characters or less 31 | * Reference issues and pull requests liberally 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alex Chaplinsky 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bozon 2 | > Command line tool for building, testing and publishing modern [Electron](http://electron.atom.io/) applications 3 | 4 | [![npm version](https://badge.fury.io/js/bozon.svg)](https://badge.fury.io/js/bozon) 5 | [![Actions Status](https://github.com/railsware/bozon/workflows/Node.js%20CI/badge.svg)](https://github.com/swiftyapp/swifty/actions) 6 | 7 | Bozon is a simple, easy to use tool that unifies the existing build tools for Electron development. Simplify building, compiling, running, testing, and packaging your Electron applications. 8 | 9 | 10 | ## Features 11 | * **Scaffolding** - Generate ready to use project structure for your new Electron application. 12 | * **Running** - Run your electron application with **Hot Reload** in development environment. 13 | * **Testing** - Build Application for test env and run feature tests for your Electron application. 14 | * **Packaging** - Build, package and publish your Electron app for Mac, Windows and Linux platforms. 15 | 16 | Bozon uses [Webpack](https://webpack.js.org) to bundle source code for **main** and **renderer** processes as well as **preload** script. It adds **webpack.config.js** file to your project so that you can further configure webpack, add new rules, loaders etc. [Jest](https://jestjs.io/) along with [Spectron](https://www.electronjs.org/spectron) are used to run your **unit** and **feature tests** within real Electron application. For **packaging** and **publishing** applications bozon uses [electron-builder](https://www.electron.build/) under the hood. 17 | 18 | ![bozon_start](https://user-images.githubusercontent.com/695947/152010984-8599ae9d-5c5c-40ec-90c5-b2a6e4d07052.png) 19 | 20 | ## Installation 21 | 22 | 23 | ```bash 24 | npm install -g bozon 25 | ``` 26 | 27 | Bozon tool should be installed globally in order to be used for all your electron apps. 28 | 29 | ## Scaffolding 30 | 31 | Then generate your new project: 32 | 33 | ```bash 34 | bozon new [name] 35 | ``` 36 | 37 | This will create a new directory `[name]` produce the following file structure: 38 | 39 | * Use `--skip-install` option if you want to skip running `npm install` 40 | 41 | ``` 42 | |--config/ 43 | |--resources/ 44 | |--src/ 45 | | |--main/ 46 | | | |--index.js 47 | | |--preload/ 48 | | | |--index.js 49 | | |--renderer/git 50 | | | |--index.html 51 | | | |--images/ 52 | | | |--stylesheets/ 53 | | | |--javascripts/ 54 | | | | |--index.js 55 | |--test/ 56 | |--package.json 57 | ``` 58 | 59 | ## Starting an application 60 | 61 | ```bash 62 | bozon start 63 | ``` 64 | 65 | This will compile Application source code to `./builds/development` directory and run your application from it. 66 | 67 | ### Configuration 68 | Bozon provides a way to define environment specific and platform specific configuration options. These multiple config files are being merged into one single `config` object during build. This `config` object is accessible via `CONFIG` variable in `main` process files of your application, so that you can use it in your code. 69 | ``` 70 | |--config/ 71 | | |--settings.json 72 | | |--environments/ 73 | | | |--development.json 74 | | | |--production.json 75 | | | |--test.json 76 | | |--platforms/ 77 | | | |--mac.json 78 | | | |--linux.json 79 | | | |--windows.json 80 | ``` 81 | 82 | ## Testing 83 | Bozon is using [Jest](https://jestjs.io/) and [Spectron](https://www.electronjs.org/spectron) for testing Electron applications. Both unit and integration tests should go to `./test` directory. Simply execute for running tests: 84 | 85 | ```bash 86 | bozon test 87 | ``` 88 | 89 | ## Packaging application 90 | Packaging Electron application is done by [electron-builder](https://www.npmjs.com/package/electron-builder) using settings in defined in `package.json` under `build` section. 91 | Application source code is being compiled to `./builds/production/` directory, and packaged versions for different platforms go to `./packages` directory. 92 | 93 | ```bash 94 | bozon package [mac|windows|linux] 95 | ``` 96 | 97 | ## License 98 | 99 | MIT © Alex Chaplinsky 100 | -------------------------------------------------------------------------------- /__mocks__/chalk.js: -------------------------------------------------------------------------------- 1 | const cyan = jest.fn(value => value) 2 | const yellow = jest.fn(value => value) 3 | const red = jest.fn(value => value) 4 | const green = jest.fn(value => value) 5 | const bold = jest.fn(value => value) 6 | 7 | export default { cyan, yellow, red, green, bold } 8 | -------------------------------------------------------------------------------- /__mocks__/child_process.js: -------------------------------------------------------------------------------- 1 | export const spawn = jest.fn() 2 | export const spawnSync = jest.fn().mockReturnValue({ status: 0 }) 3 | 4 | export default { spawn, spawnSync } 5 | -------------------------------------------------------------------------------- /__mocks__/chokidar.js: -------------------------------------------------------------------------------- 1 | const watch = jest.fn() 2 | export default { watch } 3 | -------------------------------------------------------------------------------- /__mocks__/commander.js: -------------------------------------------------------------------------------- 1 | function Commander() { 2 | Commander.prototype.version = jest.fn().mockReturnValue(this) 3 | Commander.prototype.usage = jest.fn().mockReturnValue(this) 4 | Commander.prototype.command = jest.fn().mockReturnValue(this) 5 | Commander.prototype.action = jest.fn().mockReturnValue(this) 6 | Commander.prototype.description = jest.fn().mockReturnValue(this) 7 | Commander.prototype.option = jest.fn().mockReturnValue(this) 8 | Commander.prototype.alias = jest.fn().mockReturnValue(this) 9 | Commander.prototype.parse = jest.fn().mockReturnValue(this) 10 | Commander.prototype.outputHelp = jest.fn().mockReturnValue(this) 11 | } 12 | const commander = new Commander() 13 | 14 | export default commander 15 | -------------------------------------------------------------------------------- /__mocks__/ejs.js: -------------------------------------------------------------------------------- 1 | const render = jest.fn(value => value) 2 | 3 | export default { render } 4 | -------------------------------------------------------------------------------- /__mocks__/electron-builder.js: -------------------------------------------------------------------------------- 1 | const createTarget = jest.fn().mockReturnValue('MAC_TARGET') 2 | const build = jest.fn().mockResolvedValue(true) 3 | 4 | const Platform = { 5 | MAC: { 6 | createTarget: createTarget 7 | } 8 | } 9 | const electronBuilder = { 10 | Platform: Platform, 11 | build: build 12 | } 13 | 14 | module.exports = electronBuilder 15 | -------------------------------------------------------------------------------- /__mocks__/fs-extra.js: -------------------------------------------------------------------------------- 1 | export const ensureDirSync = jest.fn() 2 | export const emptyDir = jest.fn() 3 | export const copy = jest.fn((_, out, fn) => { 4 | fn(null) 5 | }) 6 | 7 | export default { emptyDir, copy } 8 | -------------------------------------------------------------------------------- /__mocks__/fs.js: -------------------------------------------------------------------------------- 1 | let fileList = [] 2 | 3 | const __setFileList = (list) => { 4 | fileList = list 5 | } 6 | 7 | const mkdirSync = jest.fn() 8 | export const readFileSync = jest.fn((filename) => { 9 | if (filename === '/test/home/package.json') { 10 | return '{}' 11 | } 12 | return `${filename.split('/').slice(-1)[0]} contents` 13 | }) 14 | export const readdirSync = jest.fn(() => fileList) 15 | export const writeFileSync = jest.fn() 16 | export const writeFile = jest.fn((_, out, fn) => { 17 | fn(null) 18 | }) 19 | const lstatSync = jest.fn((dir) => { 20 | return { 21 | isFile: jest.fn() 22 | } 23 | }) 24 | const existsSync = jest.fn().mockReturnValue(true) 25 | 26 | export default { 27 | mkdirSync, 28 | readFileSync, 29 | writeFile, 30 | writeFileSync, 31 | lstatSync, 32 | existsSync, 33 | readdirSync, 34 | __setFileList 35 | } 36 | -------------------------------------------------------------------------------- /__mocks__/inquirer.js: -------------------------------------------------------------------------------- 1 | const prompt = jest.fn().mockResolvedValue({ 2 | name: 'myapp', author: 'John Doe' 3 | }) 4 | 5 | export default { prompt } 6 | -------------------------------------------------------------------------------- /__mocks__/latest-version.js: -------------------------------------------------------------------------------- 1 | export default jest.fn().mockResolvedValue('1.0.0') 2 | -------------------------------------------------------------------------------- /__mocks__/merge-config.js: -------------------------------------------------------------------------------- 1 | const file = jest.fn() 2 | const get = jest.fn() 3 | const Config = jest.fn(() => { 4 | return { 5 | file: file, 6 | get: get 7 | } 8 | }) 9 | Config.file = file 10 | Config.get = get 11 | 12 | export default Config 13 | -------------------------------------------------------------------------------- /__mocks__/webpack.js: -------------------------------------------------------------------------------- 1 | const stats = { 2 | compilation: { 3 | warnings: [] 4 | }, 5 | hasErrors: () => false 6 | } 7 | const webpack = jest.fn((_, fn) => { 8 | fn(null, stats) 9 | }) 10 | export default webpack 11 | -------------------------------------------------------------------------------- /__test__/builder/index.test.js: -------------------------------------------------------------------------------- 1 | import { Builder } from 'builder' 2 | 3 | import fs from 'fs' 4 | import webpack from 'webpack' 5 | import { copy } from 'fs-extra' 6 | import { startSpinner, stopSpinner } from 'utils/logger' 7 | 8 | jest.mock('fs') 9 | jest.unmock('builder') 10 | jest.mock('builder/webpack_config') 11 | jest.mock('utils/logger') 12 | 13 | describe('Builder', () => { 14 | beforeEach(async () => { 15 | fs.__setFileList(['index.html']) 16 | await Builder.run('mac', 'production') 17 | }) 18 | 19 | it('logs build start', () => { 20 | expect(startSpinner).toHaveBeenCalledWith('Building Electron application') 21 | }) 22 | 23 | it('copies html files', () => { 24 | expect(copy).toHaveBeenCalledWith( 25 | '/test/home/src/renderer/index.html', 26 | '/test/home/builds/production/renderer/index.html', 27 | expect.any(Function) 28 | ) 29 | }) 30 | 31 | it('bundles all scripts', () => { 32 | expect(webpack).toHaveBeenNthCalledWith( 33 | 1, 34 | { target: 'electron-renderer' }, 35 | expect.any(Function) 36 | ) 37 | expect(webpack).toHaveBeenNthCalledWith( 38 | 2, 39 | { target: 'electron-main' }, 40 | expect.any(Function) 41 | ) 42 | expect(webpack).toHaveBeenNthCalledWith( 43 | 3, 44 | { target: 'electron-preload' }, 45 | expect.any(Function) 46 | ) 47 | }) 48 | 49 | it('writes package.json file', () => { 50 | expect(fs.writeFile).toHaveBeenCalledWith( 51 | '/test/home/builds/production/package.json', 52 | '{"author":"Anonymous","main":"main/index.js"}', 53 | expect.any(Function) 54 | ) 55 | }) 56 | 57 | it('logs success message', () => { 58 | expect(stopSpinner).toHaveBeenCalledWith( 59 | 'Building Electron application' 60 | ) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /__test__/cleaner/index.test.js: -------------------------------------------------------------------------------- 1 | import { Cleaner } from 'cleaner' 2 | import { emptyDir } from 'fs-extra' 3 | import { startSpinner, stopSpinner } from 'utils/logger' 4 | 5 | jest.unmock('cleaner') 6 | jest.mock('utils/logger') 7 | 8 | describe('clear', () => { 9 | beforeEach(async () => await Cleaner.run()) 10 | 11 | it('shows spinner', () => { 12 | expect(startSpinner).toHaveBeenCalledWith('Cleaning app directory') 13 | }) 14 | 15 | it('clears directories', () => { 16 | expect(emptyDir).toHaveBeenNthCalledWith(1, '/test/home/builds') 17 | expect(emptyDir).toHaveBeenNthCalledWith(2, '/test/home/packages') 18 | expect(emptyDir).toHaveBeenNthCalledWith(3, '/test/home/.tmp') 19 | }) 20 | 21 | it('stops spinner with success message', () => { 22 | expect(stopSpinner).toHaveBeenCalledWith('Cleaned app directory') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /__test__/generator/index.test.js: -------------------------------------------------------------------------------- 1 | import Generator from 'generator' 2 | import Questionnaire from 'generator/questionnaire' 3 | import fs from 'fs' 4 | 5 | jest.spyOn(console, 'log').mockImplementation() 6 | jest.unmock('generator') 7 | jest.mock('generator/questionnaire') 8 | jest.mock('fs') 9 | jest.mock('child_process') 10 | 11 | describe('Generator', () => { 12 | beforeEach(async () => { 13 | const generator = new Generator('myapp', {}) 14 | await generator.generate() 15 | }) 16 | 17 | it('builds questionnaire with default name', () => { 18 | expect(Questionnaire).toHaveBeenCalledWith({ name: 'Myapp' }) 19 | }) 20 | 21 | it('calls questionnaire prompt', () => { 22 | expect(Questionnaire.prompt).toHaveBeenCalledWith(expect.any(Function)) 23 | }) 24 | 25 | it('creates directory structure', () => { 26 | [ 27 | 'myapp', 28 | 'myapp/src', 29 | 'myapp/src/main', 30 | 'myapp/src/renderer', 31 | 'myapp/src/preload', 32 | 'myapp/src/renderer/images', 33 | 'myapp/src/renderer/stylesheets', 34 | 'myapp/src/renderer/javascripts', 35 | 'myapp/config', 36 | 'myapp/config/environments', 37 | 'myapp/config/platforms', 38 | 'myapp/resources', 39 | 'myapp/test', 40 | 'myapp/test/units', 41 | 'myapp/test/features' 42 | ].forEach(dir => { 43 | expect(fs.mkdirSync).toHaveBeenCalledWith(dir) 44 | }) 45 | }) 46 | 47 | it('copies templates to directory structure', () => { 48 | [ 49 | ['/test/home/myapp/.gitignore', 'gitignore contents'], 50 | ['/test/home/myapp/package.json', 'development_package.json contents'], 51 | ['/test/home/myapp/jest.config.js', 'jest.config.js contents'], 52 | ['/test/home/myapp/webpack.config.js', 'webpack.config.js contents'], 53 | ['/test/home/myapp/LICENSE', 'license contents'], 54 | ['/test/home/myapp/README.md', 'readme.md contents'], 55 | ['/test/home/myapp/src/main/index.js', 'main.js contents'], 56 | ['/test/home/myapp/src/preload/index.js', 'preload.js contents'], 57 | ['/test/home/myapp/src/renderer/index.html', 'index.html contents'], 58 | ['/test/home/myapp/src/renderer/stylesheets/application.css', 'application.css contents'], 59 | ['/test/home/myapp/src/renderer/javascripts/index.js', 'renderer.js contents'], 60 | ['/test/home/myapp/resources/icon.icns', 'electron.icns contents'], 61 | ['/test/home/myapp/resources/icon.ico', 'electron.ico contents'], 62 | ['/test/home/myapp/config/settings.json', 'settings.json contents'], 63 | ['/test/home/myapp/config/environments/development.json', 'development.json contents'], 64 | ['/test/home/myapp/config/environments/production.json', 'production.json contents'], 65 | ['/test/home/myapp/config/environments/test.json', 'test.json contents'], 66 | ['/test/home/myapp/config/platforms/windows.json', 'windows.json contents'], 67 | ['/test/home/myapp/config/platforms/linux.json', 'linux.json contents'], 68 | ['/test/home/myapp/config/platforms/mac.json', 'mac.json contents'], 69 | ['/test/home/myapp/test/features/main.test.js', 'main_test.js contents'], 70 | ['/test/home/myapp/test/setup.js', 'setup.js contents'] 71 | ].forEach(sections => { 72 | expect(fs.writeFileSync).toHaveBeenCalledWith(sections[0], sections[1]) 73 | }) 74 | }) 75 | 76 | it('prints create file message', () => { 77 | [ 78 | ' create .gitignore', 79 | ' create package.json', 80 | ' create jest.config.js', 81 | ' create webpack.config.js', 82 | ' create LICENSE', 83 | ' create README.md', 84 | ' create src/main/index.js', 85 | ' create src/preload/index.js', 86 | ' create src/renderer/index.html', 87 | ' create src/renderer/stylesheets/application.css', 88 | ' create src/renderer/javascripts/index.js', 89 | ' create resources/icon.icns', 90 | ' create resources/icon.ico', 91 | ' create config/settings.json', 92 | ' create config/environments/development.json', 93 | ' create config/environments/production.json', 94 | ' create config/environments/test.json', 95 | ' create config/platforms/windows.json', 96 | ' create config/platforms/linux.json', 97 | ' create config/platforms/mac.json', 98 | ' create test/features/main.test.js', 99 | ' create test/setup.js' 100 | ].forEach(message => { 101 | expect(console.log).toHaveBeenCalledWith(message) 102 | }) 103 | }) 104 | 105 | it('prints post instructions', () => { 106 | expect(console.log).toHaveBeenCalledWith('Success! Created myapp at /test/home/myapp') 107 | expect(console.log).toHaveBeenCalledWith('Inside that directory, you can run several commands:') 108 | expect(console.log).toHaveBeenCalledWith(' bozon start') 109 | expect(console.log).toHaveBeenCalledWith(' Starts the Electron app in development mode.') 110 | expect(console.log).toHaveBeenCalledWith(' bozon test') 111 | expect(console.log).toHaveBeenCalledWith(' Starts the test runner.') 112 | expect(console.log).toHaveBeenCalledWith(' bozon package ') 113 | expect(console.log).toHaveBeenCalledWith(' Packages Electron application for specified platform.') 114 | expect(console.log).toHaveBeenCalledWith('We suggest you to start with typing:') 115 | expect(console.log).toHaveBeenCalledWith(' cd myapp') 116 | expect(console.log).toHaveBeenCalledWith(' bozon start') 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /__test__/generator/questionnaire.test.js: -------------------------------------------------------------------------------- 1 | import Questionnaire from 'generator/questionnaire' 2 | import inquirer from 'inquirer' 3 | 4 | jest.unmock('generator/questionnaire') 5 | jest.spyOn(console, 'log').mockImplementation() 6 | 7 | const callback = jest.fn() 8 | 9 | describe('Questionnaire', () => { 10 | beforeEach(async () => { 11 | const questionnaire = new Questionnaire({ name: 'myapp' }) 12 | await questionnaire.prompt(callback) 13 | }) 14 | 15 | it('prints welcome message', () => { 16 | expect(console.log).toHaveBeenCalledWith(' Welcome to Bozon!') 17 | expect(console.log).toHaveBeenCalledWith(' You\'re about to start new Electron application,') 18 | expect(console.log).toHaveBeenCalledWith(' but first answer a few questions about your project:') 19 | }) 20 | 21 | it('asks questions', () => { 22 | expect(inquirer.prompt).toHaveBeenCalledWith([ 23 | { 24 | default: 'myapp', 25 | message: 'What is the name of your app?', 26 | name: 'name', 27 | type: 'input', 28 | validate: expect.any(Function) 29 | }, 30 | { 31 | message: 'Please specify author name (ex: John Doe):', 32 | name: 'author', 33 | type: 'input', 34 | validate: expect.any(Function) 35 | }, 36 | { 37 | message: 'Which package manager do you use?', 38 | choices: ['yarn', 'npm'], 39 | name: 'packageManager', 40 | type: 'list' 41 | } 42 | ]) 43 | }) 44 | 45 | it('calls a callback with answers', () => { 46 | expect(callback).toHaveBeenCalledWith({ name: 'myapp', author: 'John Doe' }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /__test__/index.test.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander' 2 | 3 | import { perform } from 'index' 4 | import { create, start, build, test, pack, clear } from 'runner' 5 | 6 | jest.unmock('index') 7 | 8 | describe('bozon cli', () => { 9 | beforeEach(() => { 10 | perform() 11 | }) 12 | 13 | describe('version', () => { 14 | it('sets current version', () => { 15 | expect(commander.version).toHaveBeenCalledWith('1.3.5') 16 | }) 17 | 18 | it('sets usage instruction', () => { 19 | expect(commander.usage).toHaveBeenCalledWith('[options]') 20 | }) 21 | }) 22 | 23 | describe('new', () => { 24 | it('sets new command', () => { 25 | expect(commander.command).toHaveBeenNthCalledWith(1, 'new ') 26 | }) 27 | 28 | it('adds option to new command', () => { 29 | expect(commander.option).toHaveBeenNthCalledWith(1, '--skip-install') 30 | }) 31 | 32 | it('sets create function as action', () => { 33 | expect(commander.action).toHaveBeenCalledWith(create) 34 | }) 35 | }) 36 | 37 | describe('start', () => { 38 | it('sets start command', () => { 39 | expect(commander.command).toHaveBeenNthCalledWith(2, 'start') 40 | }) 41 | 42 | it('add alias', () => { 43 | expect(commander.alias).toHaveBeenNthCalledWith(1, 's') 44 | }) 45 | 46 | it('adds reload option to start command', () => { 47 | expect(commander.option).toHaveBeenNthCalledWith(2, '-r, --reload') 48 | }) 49 | 50 | it('adds inspect option to start command', () => { 51 | expect(commander.option).toHaveBeenNthCalledWith( 52 | 3, 53 | '-i, --inspect ' 54 | ) 55 | }) 56 | 57 | it('adds inspect-brk option to start command', () => { 58 | expect(commander.option).toHaveBeenNthCalledWith( 59 | 4, 60 | '-b, --inspect-brk ' 61 | ) 62 | }) 63 | 64 | it('sets start function as action', () => { 65 | expect(commander.action).toHaveBeenCalledWith(start) 66 | }) 67 | }) 68 | 69 | describe('build', () => { 70 | it('sets build command', () => { 71 | expect(commander.command).toHaveBeenNthCalledWith(3, 'build [env]') 72 | }) 73 | 74 | it('sets build function as action', () => { 75 | expect(commander.action).toHaveBeenCalledWith(build) 76 | }) 77 | }) 78 | 79 | describe('test', () => { 80 | it('sets test command', () => { 81 | expect(commander.command).toHaveBeenNthCalledWith(4, 'test [spec]') 82 | }) 83 | 84 | it('sets test function as action', () => { 85 | expect(commander.action).toHaveBeenCalledWith(test) 86 | }) 87 | }) 88 | 89 | describe('clear', () => { 90 | it('sets clear command', () => { 91 | expect(commander.command).toHaveBeenNthCalledWith(5, 'clear') 92 | }) 93 | 94 | it('sets clear function as action', () => { 95 | expect(commander.action).toHaveBeenCalledWith(clear) 96 | }) 97 | }) 98 | 99 | describe('package', () => { 100 | it('sets package command', () => { 101 | expect(commander.command).toHaveBeenNthCalledWith(6, 'package ') 102 | }) 103 | 104 | it('adds publish option to package command', () => { 105 | expect(commander.option).toHaveBeenNthCalledWith(5, '-p, --publish') 106 | }) 107 | 108 | it('sets pack function as action', () => { 109 | expect(commander.action).toHaveBeenCalledWith(pack) 110 | }) 111 | }) 112 | 113 | it('sets descriptions for commands', () => { 114 | expect(commander.description).toHaveBeenCalledTimes(6) 115 | }) 116 | 117 | it('sets actions for commands', () => { 118 | expect(commander.action).toHaveBeenCalledTimes(6) 119 | }) 120 | 121 | it('it parses process argv', () => { 122 | expect(commander.parse).toHaveBeenCalledWith(process.argv) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /__test__/packager/index.test.js: -------------------------------------------------------------------------------- 1 | import Packager from 'packager' 2 | import Checker from 'utils/checker' 3 | import { Builder } from 'builder' 4 | import electronBuilder from 'electron-builder' 5 | import { startSpinner, stopSpinner } from 'utils/logger' 6 | 7 | jest.unmock('packager') 8 | jest.mock('utils/checker') 9 | jest.mock('utils/logger') 10 | 11 | describe('Packager', () => { 12 | beforeEach(async () => { 13 | const packager = new Packager('mac', 'production', true) 14 | await packager.build() 15 | }) 16 | 17 | it('ensures process is run in app directory', () => { 18 | expect(Checker.ensure).toHaveBeenCalled() 19 | }) 20 | 21 | it('shows spinner', () => { 22 | expect(startSpinner).toHaveBeenCalledWith('Packaging Electron application') 23 | }) 24 | 25 | it('builds application before packaging', () => { 26 | expect(Builder.run).toHaveBeenCalledWith('mac', 'production') 27 | }) 28 | 29 | it('packages app with electron builder', () => { 30 | expect(electronBuilder.build).toHaveBeenCalledWith({ 31 | targets: 'MAC_TARGET', 32 | config: { 33 | directories: { 34 | app: 'builds/production', 35 | buildResources: 'resources', 36 | output: 'packages' 37 | } 38 | }, 39 | publish: 'always' 40 | }) 41 | }) 42 | 43 | it('stops spinner with success', () => { 44 | expect(stopSpinner).toHaveBeenCalledWith('Packaging Electron application') 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /__test__/runner/index.test.js: -------------------------------------------------------------------------------- 1 | import { create, start, pack, test, clear } from 'runner' 2 | 3 | import Generator from 'generator' 4 | import { Starter } from 'starter' 5 | import Packager from 'packager' 6 | import { TestRunner } from 'test_runner' 7 | import { Cleaner } from 'cleaner' 8 | import { restoreCursorOnExit } from 'utils' 9 | 10 | jest.spyOn(process, 'exit').mockImplementation() 11 | 12 | describe('create', () => { 13 | it('passes app name to generator', () => { 14 | create('myapp', {}) 15 | expect(Generator).toHaveBeenCalledWith('myapp', { skipInstall: false }) 16 | expect(Generator.generate).toHaveBeenCalled() 17 | }) 18 | 19 | it('passes app name and options to generator', () => { 20 | create('myapp', { skipInstall: true }) 21 | expect(Generator).toHaveBeenCalledWith('myapp', { skipInstall: true }) 22 | expect(Generator.generate).toHaveBeenCalled() 23 | }) 24 | }) 25 | 26 | describe('start', () => { 27 | it('starts the app without options', () => { 28 | start({}) 29 | expect(Starter.run).toHaveBeenCalled() 30 | }) 31 | 32 | it('starts the app with inspect option', () => { 33 | start({ inspect: true }) 34 | expect(Starter.run).toHaveBeenCalledWith({ 35 | flags: { 36 | reload: false 37 | }, 38 | options: ['--inspect=true'] 39 | }) 40 | }) 41 | 42 | it('restores cursor', () => { 43 | start({}) 44 | expect(restoreCursorOnExit).toHaveBeenCalled() 45 | }) 46 | }) 47 | 48 | describe('test', () => { 49 | it('calls test runner without options', async () => { 50 | await test() 51 | expect(TestRunner.run).toHaveBeenCalled() 52 | }) 53 | 54 | it('calls test runner with path', async () => { 55 | await test('test/index.test.js') 56 | expect(TestRunner.run).toHaveBeenCalledWith('test/index.test.js') 57 | }) 58 | 59 | it('restores cursor', async () => { 60 | await test() 61 | expect(restoreCursorOnExit).toHaveBeenCalled() 62 | }) 63 | 64 | it('exits with success status', async () => { 65 | await test() 66 | expect(process.exit).toHaveBeenCalledWith(0) 67 | }) 68 | 69 | describe('running test fails', () => { 70 | beforeEach(() => { 71 | TestRunner.__setError(Error) 72 | }) 73 | 74 | it('exits with error status', async () => { 75 | await test() 76 | expect(process.exit).toHaveBeenCalledWith(1) 77 | }) 78 | }) 79 | }) 80 | 81 | describe('package', () => { 82 | it('calls packager with publish option', () => { 83 | pack('mac', { publish: true }) 84 | expect(Packager).toHaveBeenCalledWith('mac', 'production', true) 85 | expect(Packager.build).toHaveBeenCalled() 86 | }) 87 | 88 | it('calls packager without publish option', () => { 89 | pack('windows', {}) 90 | expect(Packager).toHaveBeenCalledWith('windows', 'production', false) 91 | expect(Packager.build).toHaveBeenCalled() 92 | }) 93 | 94 | it('restores cursor', () => { 95 | pack('mac', {}) 96 | expect(restoreCursorOnExit).toHaveBeenCalled() 97 | }) 98 | }) 99 | 100 | describe('clear', () => { 101 | it('calls test runner without options', () => { 102 | clear() 103 | expect(Cleaner.run).toHaveBeenCalled() 104 | }) 105 | 106 | it('restores cursor', () => { 107 | clear() 108 | expect(restoreCursorOnExit).toHaveBeenCalled() 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /__test__/setup.js: -------------------------------------------------------------------------------- 1 | process.cwd = jest.fn().mockReturnValue('/test/home') 2 | Object.defineProperty(process, 'platform', { 3 | get: () => 'linux', 4 | set: jest.fn() 5 | }) 6 | -------------------------------------------------------------------------------- /__test__/starter/index.test.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import { Starter } from 'starter' 3 | import { Builder } from 'builder' 4 | import Checker from 'utils/checker' 5 | import { startSpinner, stopSpinner } from 'utils/logger' 6 | 7 | jest.unmock('starter') 8 | 9 | jest.mock('child_process') 10 | jest.mock('utils/logger') 11 | jest.mock('utils/checker') 12 | 13 | const setup = async flags => { 14 | await Starter.run({ flags: flags, options: [{ inspect: true }] }) 15 | } 16 | 17 | describe('Starter', () => { 18 | describe('Build successful', () => { 19 | describe('with reload flag', () => { 20 | beforeEach(() => setup({ reload: true })) 21 | 22 | it('ensures process is run in app directory', () => { 23 | expect(Checker.ensure).toHaveBeenCalled() 24 | }) 25 | 26 | it('logs application start', () => { 27 | expect(startSpinner).toHaveBeenCalledWith('Starting application') 28 | }) 29 | 30 | it('runs builder with platform and env', () => { 31 | expect(Builder.run).toHaveBeenCalledWith( 32 | 'linux', 33 | 'development', 34 | { 35 | reload: true 36 | } 37 | ) 38 | }) 39 | 40 | it('runs electron app', () => { 41 | expect(spawn).toHaveBeenCalledWith( 42 | 'npx', 43 | ['nodemon', '-w builds/development/main', '-e js', 44 | '-q', 'node_modules/.bin/electron', 'builds/development', { inspect: true }], 45 | { env: {}, shell: true, stdio: 'inherit' } 46 | ) 47 | }) 48 | 49 | it('stops spinner with success', () => { 50 | expect(stopSpinner).toHaveBeenCalledWith('Starting application') 51 | }) 52 | }) 53 | 54 | describe('without reload flag', () => { 55 | beforeEach(() => setup({ reload: false })) 56 | 57 | it('runs builder with platform and env', () => { 58 | expect(Builder.run).toHaveBeenCalledWith( 59 | 'linux', 60 | 'development', 61 | { 62 | reload: false 63 | } 64 | ) 65 | }) 66 | 67 | it('runs electron app', () => { 68 | expect(spawn).toHaveBeenCalledWith( 69 | 'npx', 70 | ['electron', 'builds/development', { inspect: true }], 71 | { env: {}, shell: true, stdio: 'inherit' } 72 | ) 73 | }) 74 | }) 75 | }) 76 | 77 | describe('Build unsuccessful', () => { 78 | beforeEach(() => { 79 | Builder.__setBuildError('Unknown error') 80 | setup() 81 | }) 82 | 83 | it('should not log application start', () => { 84 | expect(startSpinner).toHaveBeenCalledTimes(0) 85 | }) 86 | 87 | it('should not run electron app', () => { 88 | expect(spawn).toHaveBeenCalledTimes(0) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /__test__/test_runner/index.test.js: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process' 2 | import { TestRunner } from 'test_runner' 3 | import Checker from 'utils/checker' 4 | import { log } from 'utils/logger' 5 | 6 | jest.mock('fs') 7 | jest.mock('child_process') 8 | jest.mock('utils/checker') 9 | jest.mock('utils/logger') 10 | jest.unmock('test_runner') 11 | 12 | describe('TestRunner', () => { 13 | describe('with no path argument', () => { 14 | beforeEach(async () => { 15 | await TestRunner.run() 16 | }) 17 | 18 | it('ensures process is run in app directory', () => { 19 | expect(Checker.ensure).toHaveBeenCalled() 20 | }) 21 | 22 | it('shows spinner', () => { 23 | expect(log).toHaveBeenCalledWith('Running test suite...') 24 | }) 25 | 26 | it('runs jest twice', () => { 27 | expect(spawnSync).toHaveBeenCalledTimes(2) 28 | }) 29 | 30 | it('runs jest for unit tests', () => { 31 | expect(spawnSync).toHaveBeenNthCalledWith(1, 'npx', ['jest', './test/units'], { 32 | env: {}, 33 | shell: true, 34 | stdio: 'inherit' 35 | }) 36 | }) 37 | 38 | it('runs jest for feature tests', () => { 39 | expect(spawnSync).toHaveBeenNthCalledWith(2, 'npx', ['jest', '-i', './test/features'], { 40 | env: {}, 41 | shell: true, 42 | stdio: 'inherit' 43 | }) 44 | }) 45 | }) 46 | 47 | describe('with path argument', () => { 48 | describe('with path to unit test', () => { 49 | beforeEach(async () => { 50 | await TestRunner.run('test/units/some.test.js') 51 | }) 52 | 53 | it('ensures process is run in app directory', () => { 54 | expect(Checker.ensure).toHaveBeenCalled() 55 | }) 56 | 57 | it('shows spinner', () => { 58 | expect(log).toHaveBeenCalledWith('Running test suite...') 59 | }) 60 | 61 | it('runs jest once', () => { 62 | expect(spawnSync).toHaveBeenCalledTimes(1) 63 | }) 64 | 65 | it('runs jest for unit tests', () => { 66 | expect(spawnSync).toHaveBeenNthCalledWith(1, 'npx', ['jest', 'test/units/some.test.js'], { 67 | env: {}, 68 | shell: true, 69 | stdio: 'inherit' 70 | }) 71 | }) 72 | }) 73 | 74 | describe('with path to feature test', () => { 75 | beforeEach(async () => { 76 | await TestRunner.run('test/features/some.test.js') 77 | }) 78 | 79 | it('ensures process is run in app directory', () => { 80 | expect(Checker.ensure).toHaveBeenCalled() 81 | }) 82 | 83 | it('shows spinner', () => { 84 | expect(log).toHaveBeenCalledWith('Running test suite...') 85 | }) 86 | 87 | it('runs jest once', () => { 88 | expect(spawnSync).toHaveBeenCalledTimes(1) 89 | }) 90 | 91 | it('runs jest for feature tests', () => { 92 | expect(spawnSync).toHaveBeenNthCalledWith(1, 'npx', ['jest', '-i', 'test/features/some.test.js'], { 93 | env: {}, 94 | shell: true, 95 | stdio: 'inherit' 96 | }) 97 | }) 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /__test__/utils/checker.test.js: -------------------------------------------------------------------------------- 1 | import Checker from 'utils/checker' 2 | import fs from 'fs' 3 | 4 | jest.mock('fs') 5 | 6 | describe('Checker', () => { 7 | beforeEach(() => Checker.ensure()) 8 | 9 | it('checks for package.json', () => { 10 | expect(fs.lstatSync).toHaveBeenCalledWith('/test/home/package.json') 11 | }) 12 | 13 | it('checks for node_modules', () => { 14 | expect(fs.lstatSync).toHaveBeenCalledWith('/test/home/node_modules') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /__test__/utils/index.test.js: -------------------------------------------------------------------------------- 1 | import { config, platform, source, sourcePath, destinationPath } from 'utils' 2 | 3 | import Config from 'merge-config' 4 | 5 | jest.spyOn(console, 'log').mockImplementation() 6 | jest.mock('child_process') 7 | jest.unmock('utils') 8 | 9 | describe('utils', () => { 10 | describe('config', () => { 11 | it('builds config for mac and production', () => { 12 | config('production', 'mac') 13 | expect(Config).toHaveBeenCalled() 14 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/settings.json') 15 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/environments/production.json') 16 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/platforms/mac.json') 17 | }) 18 | 19 | it('builds config for mac and test', () => { 20 | config('test', 'mac') 21 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/settings.json') 22 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/environments/test.json') 23 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/platforms/mac.json') 24 | }) 25 | 26 | it('builds config for linux and production', () => { 27 | config('production', 'linux') 28 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/settings.json') 29 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/environments/production.json') 30 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/platforms/linux.json') 31 | }) 32 | 33 | it('builds config for windows and test', () => { 34 | config('production', 'linux') 35 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/settings.json') 36 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/environments/production.json') 37 | expect(Config.file).toHaveBeenCalledWith('/test/home/config/platforms/linux.json') 38 | }) 39 | }) 40 | 41 | describe('source', () => { 42 | it('returns file path in src directory', () => { 43 | expect(source('package.json')).toBe('/test/home/package.json') 44 | }) 45 | }) 46 | 47 | describe('platform', () => { 48 | it('returns current platform', () => { 49 | expect(platform()).toBe('linux') 50 | }) 51 | }) 52 | 53 | describe('sourcePath', () => { 54 | it('returns source path to file', () => { 55 | expect(sourcePath('index.js')).toBe('/test/home/src/index.js') 56 | }) 57 | }) 58 | 59 | describe('destinationPath', () => { 60 | it('returns path to destination file', () => { 61 | expect(destinationPath('index.js', 'development')).toBe( 62 | '/test/home/builds/development/index.js' 63 | ) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current' 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /bin/bozon: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | require('../dist/index.js').perform() 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/z_/3ndgqq0n609bqbnfhw41z2fr0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: undefined, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: undefined, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: undefined, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: undefined, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: undefined, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: undefined, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | moduleDirectories: ['node_modules', 'src'], 68 | 69 | // An array of file extensions your modules use 70 | // moduleFileExtensions: [ 71 | // "js", 72 | // "json", 73 | // "jsx", 74 | // "ts", 75 | // "tsx", 76 | // "node" 77 | // ], 78 | 79 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 80 | // moduleNameMapper: {}, 81 | 82 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 83 | // modulePathIgnorePatterns: [], 84 | 85 | // Activates notifications for test results 86 | // notify: false, 87 | 88 | // An enum that specifies notification mode. Requires { notify: true } 89 | // notifyMode: "failure-change", 90 | 91 | // A preset that is used as a base for Jest's configuration 92 | // preset: undefined, 93 | 94 | // Run tests from one or more projects 95 | // projects: undefined, 96 | 97 | // Use this configuration option to add custom reporters to Jest 98 | // reporters: undefined, 99 | 100 | // Automatically reset mock state between every test 101 | // resetMocks: false, 102 | 103 | // Reset the module registry before running each individual test 104 | // resetModules: false, 105 | 106 | // A path to a custom resolver 107 | // resolver: undefined, 108 | 109 | // Automatically restore mock state between every test 110 | // restoreMocks: false, 111 | 112 | // The root directory that Jest should scan for tests and modules within 113 | // rootDir: undefined, 114 | 115 | // A list of paths to directories that Jest should use to search for files in 116 | // roots: [ 117 | // "" 118 | // ], 119 | 120 | // Allows you to use a custom runner instead of Jest's default test runner 121 | // runner: "jest-runner", 122 | 123 | // The paths to modules that run some code to configure or set up the testing environment before each test 124 | setupFiles: ['/__test__/setup.js'], 125 | 126 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 127 | // setupFilesAfterEnv: [], 128 | 129 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 130 | // snapshotSerializers: [], 131 | 132 | // The test environment that will be used for testing 133 | testEnvironment: 'node' 134 | 135 | // Options that will be passed to the testEnvironment 136 | // testEnvironmentOptions: {}, 137 | 138 | // Adds a location field to test results 139 | // testLocationInResults: false, 140 | 141 | // The glob patterns Jest uses to detect test files 142 | // testMatch: [ 143 | // "**/__tests__/**/*.[jt]s?(x)", 144 | // "**/?(*.)+(spec|test).[tj]s?(x)" 145 | // ], 146 | 147 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 148 | // testPathIgnorePatterns: [ 149 | // "/node_modules/" 150 | // ], 151 | 152 | // The regexp pattern or array of patterns that Jest uses to detect test files 153 | // testRegex: [], 154 | 155 | // This option allows the use of a custom results processor 156 | // testResultsProcessor: undefined, 157 | 158 | // This option allows use of a custom test runner 159 | // testRunner: "jasmine2", 160 | 161 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 162 | // testURL: "http://localhost", 163 | 164 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 165 | // timers: "real", 166 | 167 | // A map from regular expressions to paths to transformers 168 | // transform: undefined, 169 | 170 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 171 | // transformIgnorePatterns: [ 172 | // "/node_modules/" 173 | // ], 174 | 175 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 176 | // unmockedModulePathPatterns: undefined, 177 | 178 | // Indicates whether each individual test should be reported during the run 179 | // verbose: undefined, 180 | 181 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 182 | // watchPathIgnorePatterns: [], 183 | 184 | // Whether to use watchman for file crawling 185 | // watchman: true, 186 | } 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bozon", 3 | "version": "1.3.5", 4 | "description": "Command line tool for building, testing and publishing modern Electron applications", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/railsware/bozon.git" 8 | }, 9 | "main": "lib/index.js", 10 | "scripts": { 11 | "dev": "npx webpack --watch", 12 | "build": "npx webpack --mode production", 13 | "lint": "npx eslint src", 14 | "test": "npx jest", 15 | "publish": "npm publish", 16 | "release": "yarn build && yarn publish" 17 | }, 18 | "keywords": [ 19 | "electron", 20 | "desktop", 21 | "application", 22 | "cli", 23 | "bozon" 24 | ], 25 | "author": "Railsware, Alex Chaplinsky", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@electron/notarize": "^1.2.3", 29 | "chalk": "^4.1.2", 30 | "chokidar": "^3.5.3", 31 | "commander": "^9.4.1", 32 | "css-loader": "^6.7.1", 33 | "ejs": "^3.1.8", 34 | "electron": "^21.2.0", 35 | "electron-builder": "^23.6.0", 36 | "elliptic": "^6.5.4", 37 | "got": "^11.8.3", 38 | "inquirer": "^8.2.4", 39 | "latest-version": "^5.1.0", 40 | "lodash": "^4.17.21", 41 | "merge-config": "^2.0.0", 42 | "nodemon": "^2.0.20", 43 | "style-loader": "^3.3.1", 44 | "underscore.string": "^3.3.6", 45 | "webpack": "^5.74.0", 46 | "webpack-inject-plugin": "^1.5.5", 47 | "webpack-merge": "^5.8.0" 48 | }, 49 | "preferGlobal": true, 50 | "bin": { 51 | "bozon": "bin/bozon" 52 | }, 53 | "engines": { 54 | "node": ">= 10.0.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.19.6", 58 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 59 | "@babel/preset-env": "^7.19.4", 60 | "babel-jest": "^29.2.2", 61 | "babel-loader": "^9.0.0", 62 | "eslint": "^8.26.0", 63 | "eslint-config-standard": "^17.0.0", 64 | "eslint-plugin-import": "^2.26.0", 65 | "eslint-plugin-n": "^15.4.0", 66 | "eslint-plugin-node": "^11.1.0", 67 | "eslint-plugin-promise": "^6.1.1", 68 | "eslint-plugin-standard": "^5.0.0", 69 | "fs-extra": "^10.1.0", 70 | "jest": "^29.2.2", 71 | "prettier": "^2.7.1", 72 | "webpack-cli": "^4.10.0", 73 | "webpack-node-externals": "^3.0.0" 74 | }, 75 | "resolutions": { 76 | "js-yaml": "^4.1.0", 77 | "yargs-parser": "^20.2.9", 78 | "lodash": "^4.17.21", 79 | "minimatch": ">=3.0.5", 80 | "terser": "^5.14.2", 81 | "xmldom": ">=0.5.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/__mocks__/builder.js: -------------------------------------------------------------------------------- 1 | let buildError 2 | 3 | export const Builder = { 4 | run: jest.fn(() => { 5 | if (buildError) { 6 | return Promise.reject(Error(buildError)) 7 | } 8 | return Promise.resolve() 9 | }), 10 | __setBuildError: message => { 11 | buildError = message 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/__mocks__/cleaner.js: -------------------------------------------------------------------------------- 1 | export const Cleaner = { run: jest.fn() } 2 | -------------------------------------------------------------------------------- /src/__mocks__/generator.js: -------------------------------------------------------------------------------- 1 | const Generator = jest.fn() 2 | Generator.generate = jest.fn() 3 | 4 | Generator.mockReturnValue({ 5 | generate: Generator.generate 6 | }) 7 | 8 | export default Generator 9 | -------------------------------------------------------------------------------- /src/__mocks__/packager.js: -------------------------------------------------------------------------------- 1 | const Packager = jest.fn() 2 | Packager.build = jest.fn().mockResolvedValue(true) 3 | 4 | Packager.mockReturnValue({ 5 | build: Packager.build 6 | }) 7 | 8 | export default Packager 9 | -------------------------------------------------------------------------------- /src/__mocks__/starter.js: -------------------------------------------------------------------------------- 1 | export const Starter = { run: jest.fn() } 2 | -------------------------------------------------------------------------------- /src/__mocks__/test_runner.js: -------------------------------------------------------------------------------- 1 | let testError = null 2 | 3 | export const TestRunner = { 4 | __setError: error => { 5 | testError = error 6 | }, 7 | run: jest.fn(() => { 8 | if (testError) { 9 | return Promise.reject(testError) 10 | } else { 11 | return Promise.resolve() 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/__mocks__/utils.js: -------------------------------------------------------------------------------- 1 | export const isWindows = jest.fn().mockReturnValue(false) 2 | export const isMacOS = jest.fn().mockReturnValue(false) 3 | export const isLinux = jest.fn().mockReturnValue(true) 4 | export const platform = jest.fn().mockReturnValue('linux') 5 | export const source = jest.fn(value => `/test/home/${value}`) 6 | export const sourcePath = jest.fn(value => `/test/home/src/${value}`) 7 | export const nodeEnv = jest.fn().mockReturnValue({}) 8 | export const restoreCursorOnExit = jest.fn() 9 | export const destinationPath = jest.fn( 10 | (value, env) => `/test/home/builds/${env}/${value}` 11 | ) 12 | -------------------------------------------------------------------------------- /src/builder/bundle.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | 3 | export const bundle = (config) => { 4 | return new Promise((resolve, reject) => { 5 | webpack(config, (error, stats) => { 6 | if (error || stats.hasErrors()) { 7 | return reject(error || stats.compilation.errors) 8 | } 9 | const { compilation: { warnings } } = stats 10 | return resolve(warnings.length > 0 ? warnings.join() : null) 11 | }) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/builder/html.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readdirSync } from 'fs' 3 | import { copy } from 'fs-extra' 4 | 5 | export const buildHTML = (inputDir, outputDir) => { 6 | Promise.all( 7 | readdirSync(inputDir).filter((file) => { 8 | if (file.match(/\.html$/)) { 9 | return copyHTMLFile(path.join(inputDir, file), path.join(outputDir, file)) 10 | } 11 | return false 12 | }) 13 | ) 14 | } 15 | 16 | export const copyHTMLFile = (input, output) => { 17 | return new Promise((resolve, reject) => { 18 | copy(input, output, (error) => { 19 | if (error) { 20 | return reject(error) 21 | } else { 22 | return resolve(null) 23 | } 24 | }) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/builder/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { ensureDirSync } from 'fs-extra' 3 | import WebpackConfig from 'builder/webpack_config' 4 | import { buildManifest } from 'builder/manifest' 5 | import { buildHTML } from 'builder/html' 6 | import { bundle } from 'builder/bundle' 7 | import { watch } from 'builder/watcher' 8 | import { source, sourcePath, destinationPath } from 'utils' 9 | import { log, warn, startSpinner, stopSpinner } from 'utils/logger' 10 | import { inspect } from 'util' 11 | 12 | const BUILD_START = 'Building Electron application' 13 | const BUILD_FAILED = 'Failed to build application' 14 | const BUILD_SUCCEED = 'Building Electron application' 15 | 16 | const run = (platform, env, flags) => { 17 | startSpinner(BUILD_START) 18 | const config = new WebpackConfig(env, platform, flags).build() 19 | ensureDirSync(destinationPath('', env)) 20 | 21 | return Promise.all(buildQueue(config, env, flags)) 22 | .then(warnings => { 23 | onBuildSuccess(config, env, warnings) 24 | }) 25 | .catch(onBuildError) 26 | } 27 | 28 | const onBuildSuccess = (config, env, warnings) => { 29 | stopSpinner(BUILD_SUCCEED) 30 | onBuildWarnings(warnings) 31 | if (env === 'development') { 32 | watch(config, env) 33 | } 34 | } 35 | 36 | const onBuildError = error => { 37 | stopSpinner(BUILD_FAILED, false) 38 | log(chalk.grey(inspect(error))) 39 | process.stdin.end() 40 | process.kill(process.pid) 41 | } 42 | 43 | const onBuildWarnings = warnings => { 44 | warnings.forEach(item => { 45 | if (item) warn(item) 46 | }) 47 | } 48 | 49 | const buildQueue = (config, env) => { 50 | return [ 51 | buildHTML(sourcePath('renderer'), destinationPath('renderer', env)), 52 | buildManifest(source('package.json'), destinationPath('package.json', env)), 53 | bundle(config.renderer), 54 | bundle(config.main), 55 | bundle(config.preload) 56 | ] 57 | } 58 | 59 | export const Builder = { run } 60 | -------------------------------------------------------------------------------- /src/builder/manifest.js: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFile } from 'fs' 2 | 3 | export const buildManifest = (source, destination) => { 4 | return new Promise((resolve, reject) => { 5 | const json = JSON.parse(readFileSync(source)) 6 | const settings = { 7 | name: json.name, 8 | version: json.version, 9 | description: json.description, 10 | author: json.author || 'Anonymous', 11 | main: 'main/index.js', 12 | repository: json.repository 13 | } 14 | writeFile(destination, JSON.stringify(settings), (err) => { 15 | if (err) { 16 | reject(err) 17 | } else { 18 | resolve(null) 19 | } 20 | }) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/builder/messages.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | export const BUILD_START = chalk.cyan('Building Electron application') 4 | export const BUILD_FAILED = `${chalk.cyan('Building Electron application:')} ${chalk.yellow('Failed')}` 5 | export const BUILD_SUCCEED = `${chalk.cyan('Building Electron application:')} ${chalk.green('Done')}` 6 | -------------------------------------------------------------------------------- /src/builder/watcher.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import chalk from 'chalk' 3 | import chokidar from 'chokidar' 4 | import { copyHTMLFile } from 'builder/html' 5 | import { bundle } from 'builder/bundle' 6 | import { source, sourcePath, destinationPath } from 'utils' 7 | import { log, startSpinner, stopSpinner } from 'utils/logger' 8 | 9 | const MAIN_KEY = '~MAIN~' 10 | const RENDER_EKY = 'RENDER' 11 | const PRELOAD_KEY = '~PREL~' 12 | 13 | export const watch = (config, env) => { 14 | const watcher = chokidar.watch(sourcePath('**/*.*'), { 15 | ignored: /node_modules/, 16 | persistent: true 17 | }) 18 | 19 | watcher.on('ready', () => { 20 | log(`${chalk.cyan('Watching for changes')} 👀\n`) 21 | }) 22 | 23 | watcher.on('change', (file) => handleChange(file, config, env)) 24 | } 25 | 26 | const handleChange = (file, config, env) => { 27 | log(fileChangedMessage(path.relative(source(), file))) 28 | if (file.match(/src\/main/)) { 29 | processChange(MAIN_KEY, bundle(config.main)) 30 | } else if (file.match(/src\/preload/)) { 31 | processChange(PRELOAD_KEY, bundle(config.preload)) 32 | } else if (file.match(/\.html$/)) { 33 | log(compilingMessage(RENDER_EKY)) 34 | processChange(RENDER_EKY, copyHTMLFile(file, htmlDestination(file, env))) 35 | } else { 36 | processChange(RENDER_EKY, bundle(config.renderer)) 37 | } 38 | } 39 | 40 | const processChange = (key, processing) => { 41 | startSpinner(compilingMessage(key)) 42 | const start = new Date() 43 | processing.then(() => { 44 | const end = new Date() 45 | const time = end.getTime() - start.getTime() 46 | stopSpinner(compilationDoneMessage(key, time)) 47 | }) 48 | } 49 | 50 | const htmlDestination = (file, env) => 51 | destinationPath(path.join('renderer', path.parse(file).base), env) 52 | 53 | const fileChangedMessage = (file) => 54 | `[${chalk.green('CHANGE')}] ${chalk.grey('File')} ${chalk.bold( 55 | file 56 | )} ${chalk.grey('has been changed')}` 57 | 58 | const compilingMessage = (key) => 59 | `[${chalk.grey(key)}] ${chalk.grey('Compiling')}` 60 | 61 | const compilationDoneMessage = (key, time) => 62 | `[${chalk.grey(key)}] ${chalk.cyan('Compiled')} ${chalk.grey( 63 | 'in' 64 | )} ${time} ${chalk.grey('ms')}` 65 | -------------------------------------------------------------------------------- /src/builder/webpack_config/__mocks__/index.js: -------------------------------------------------------------------------------- 1 | const WebpackConfig = jest.fn() 2 | WebpackConfig.build = jest.fn(() => { 3 | return { 4 | renderer: { 5 | target: 'electron-renderer' 6 | }, 7 | main: { 8 | target: 'electron-main' 9 | }, 10 | preload: { 11 | target: 'electron-preload' 12 | } 13 | } 14 | }) 15 | 16 | WebpackConfig.mockReturnValue({ 17 | build: WebpackConfig.build 18 | }) 19 | 20 | export default WebpackConfig 21 | -------------------------------------------------------------------------------- /src/builder/webpack_config/defaults.js: -------------------------------------------------------------------------------- 1 | import { destinationPath } from 'utils' 2 | 3 | export const mainDefaults = (mode, env) => { 4 | return { 5 | mode, 6 | target: 'electron-main', 7 | entry: './src/main/index.js', 8 | output: { 9 | path: destinationPath('main', env), 10 | filename: 'index.js' 11 | }, 12 | node: { 13 | __dirname: false, 14 | __filename: false 15 | }, 16 | resolve: { 17 | modules: [ 18 | 'node_modules', 19 | './src/main', 20 | './resources' 21 | ] 22 | }, 23 | plugins: [] 24 | } 25 | } 26 | 27 | export const rendererDefaults = (mode, env) => { 28 | return { 29 | mode, 30 | target: 'electron-renderer', 31 | entry: './src/renderer/javascripts/index.js', 32 | output: { 33 | path: destinationPath('renderer', env), 34 | filename: 'index.js' 35 | }, 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.css$/, 40 | use: [ 41 | { 42 | loader: 'style-loader' 43 | }, 44 | { 45 | loader: 'css-loader' 46 | } 47 | ] 48 | } 49 | ] 50 | }, 51 | resolve: { 52 | modules: [ 53 | 'node_modules', 54 | './src/renderer/javascripts', 55 | './src/renderer/stylesheets', 56 | './src/renderer/images' 57 | ] 58 | }, 59 | plugins: [] 60 | } 61 | } 62 | 63 | export const preloadDefaults = (mode, env) => { 64 | return { 65 | mode, 66 | target: 'electron-preload', 67 | entry: './src/preload/index.js', 68 | output: { 69 | path: destinationPath('preload', env), 70 | filename: 'index.js' 71 | }, 72 | node: { 73 | __dirname: false, 74 | __filename: false 75 | }, 76 | plugins: [] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/builder/webpack_config/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import webpack from 'webpack' 4 | import { mergeWithCustomize } from 'webpack-merge' 5 | import InjectPlugin from 'webpack-inject-plugin' 6 | import { source, config } from 'utils' 7 | import { mainDefaults, rendererDefaults, preloadDefaults } from './defaults' 8 | 9 | const UNIQUENESS_KEYS = ['resolve.modules'] 10 | 11 | export default class WebpackConfig { 12 | constructor(env, platform, flags) { 13 | this.env = env 14 | this.platform = platform 15 | this.flags = flags 16 | this.localConfig() 17 | } 18 | 19 | build() { 20 | const configs = { 21 | main: this.merge(mainDefaults(this.mode(), this.env), this.config.main), 22 | renderer: this.merge( 23 | rendererDefaults(this.mode(), this.env), 24 | this.config.renderer 25 | ), 26 | preload: this.merge( 27 | preloadDefaults(this.mode(), this.env), 28 | this.config.preload 29 | ) 30 | } 31 | this.injectConfig(configs) 32 | if (this.env === 'development' && this.flags.reload) { 33 | this.injectDevScript(configs) 34 | } 35 | return configs 36 | } 37 | 38 | merge(defaults, config) { 39 | return mergeWithCustomize({ 40 | customizeArray(a, b, key) { 41 | if (UNIQUENESS_KEYS.indexOf(key) !== -1) { 42 | return Array.from(new Set([...a, ...b])) 43 | } else if (key === 'module.rules') { 44 | const tests = b.map((obj) => obj.test.toString()) 45 | return [ 46 | ...a.filter((obj) => tests.indexOf(obj.test.toString()) === -1), 47 | ...b 48 | ] 49 | } 50 | } 51 | })(defaults, config) 52 | } 53 | 54 | injectConfig(configs) { 55 | const CONFIG = { 56 | CONFIG: JSON.stringify(this.settings()) 57 | } 58 | configs.main.plugins.push(new webpack.DefinePlugin(CONFIG)) 59 | configs.preload.plugins.push(new webpack.DefinePlugin(CONFIG)) 60 | } 61 | 62 | injectDevScript(configs) { 63 | configs.main.plugins.push( 64 | new InjectPlugin(() => { 65 | return fs.readFileSync(path.resolve(__dirname, 'dev.js')).toString() 66 | }) 67 | ) 68 | } 69 | 70 | mode() { 71 | return (this.env === 'development' || this.env === 'test') ? 'development' : 'production' 72 | } 73 | 74 | settings() { 75 | const json = JSON.parse(fs.readFileSync(source('package.json'))) 76 | const settings = config(this.env, this.platform) 77 | settings.name = json.name 78 | settings.version = json.version 79 | return settings 80 | } 81 | 82 | localConfig() { 83 | const configFile = source('webpack.config.js') 84 | this.config = fs.existsSync(configFile) 85 | ? __non_webpack_require__(configFile) 86 | : {} 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/cleaner/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { emptyDir } from 'fs-extra' 3 | import { startSpinner, stopSpinner } from 'utils/logger' 4 | 5 | const DIRECTORIES = ['builds', 'packages', '.tmp'] 6 | 7 | const run = async () => { 8 | startSpinner('Cleaning app directory') 9 | await Promise.all(DIRECTORIES.map((dir) => clearDir(dir))) 10 | stopSpinner('Cleaned app directory') 11 | } 12 | 13 | const clearDir = (dir) => { 14 | return emptyDir(path.join(process.cwd(), dir)) 15 | } 16 | 17 | export const Cleaner = { run } 18 | -------------------------------------------------------------------------------- /src/dev/index.js: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | const browserWindows = [] 6 | 7 | const reloadRenderer = () => { 8 | Object.values(browserWindows).forEach(window => { 9 | if (window) window.webContents.reloadIgnoringCache() 10 | }) 11 | } 12 | 13 | app.on('browser-window-created', (_, bw) => { 14 | browserWindows.push(bw) 15 | bw.on('closed', () => { 16 | console.log(browserWindows.indexOf(bw)) 17 | browserWindows.splice(browserWindows.indexOf(bw), 1) 18 | }) 19 | }) 20 | 21 | fs.watch(path.resolve(__dirname, '..', 'renderer'), {}, reloadRenderer) 22 | -------------------------------------------------------------------------------- /src/generator/__mocks__/questionnaire.js: -------------------------------------------------------------------------------- 1 | const Questionnaire = jest.fn() 2 | Questionnaire.prompt = jest.fn(fn => { 3 | fn({ name: 'myapp', author: 'John Doe' }) 4 | }) 5 | 6 | Questionnaire.mockReturnValue({ 7 | prompt: Questionnaire.prompt 8 | }) 9 | 10 | export default Questionnaire 11 | -------------------------------------------------------------------------------- /src/generator/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import childProcess from 'child_process' 3 | import chalk from 'chalk' 4 | import ejs from 'ejs' 5 | import fs from 'fs' 6 | import latestVersion from 'latest-version' 7 | import { classify, underscored } from 'underscore.string' 8 | import Questionnaire from './questionnaire' 9 | 10 | const $ = path.join 11 | 12 | export default class Generator { 13 | constructor(name, options) { 14 | this.name = underscored(name) 15 | this.options = options 16 | this.defaults = { 17 | id: 'bozonapp', 18 | name: classify(name), 19 | author: null, 20 | year: new Date().getFullYear() 21 | } 22 | this.questionnaire = new Questionnaire({ name: this.defaults.name }) 23 | } 24 | 25 | async generate() { 26 | await this.getVersions() 27 | return this.questionnaire.prompt(async (answers) => { 28 | this.defaults.id = answers.name.toLowerCase() 29 | this.defaults.name = classify(answers.name) 30 | this.defaults.packageManager = answers.packageManager 31 | this.defaults.author = answers.author 32 | this.setup() 33 | }) 34 | } 35 | 36 | setup() { 37 | this.createDirectories() 38 | this.copyTemplates() 39 | this.installPackages() 40 | this.printInstructions() 41 | } 42 | 43 | createDirectories() { 44 | this.mkdir(this.name) 45 | this.mkdir(this.name, 'src') 46 | this.mkdir(this.name, 'src', 'main') 47 | this.mkdir(this.name, 'src', 'renderer') 48 | this.mkdir(this.name, 'src', 'preload') 49 | this.mkdir(this.name, 'src', 'renderer', 'images') 50 | this.mkdir(this.name, 'src', 'renderer', 'stylesheets') 51 | this.mkdir(this.name, 'src', 'renderer', 'javascripts') 52 | this.mkdir(this.name, 'config') 53 | this.mkdir(this.name, 'config', 'environments') 54 | this.mkdir(this.name, 'config', 'platforms') 55 | this.mkdir(this.name, 'resources') 56 | this.mkdir(this.name, 'test') 57 | this.mkdir(this.name, 'test', 'units') 58 | this.mkdir(this.name, 'test', 'features') 59 | } 60 | 61 | async getVersions() { 62 | this.defaults.bozonVersion = await latestVersion('bozon') 63 | this.defaults.jestVersion = await latestVersion('jest') 64 | this.defaults.spectronVersion = await latestVersion('spectron') 65 | } 66 | 67 | copyTemplates() { 68 | this.copyTpl('gitignore', '.gitignore') 69 | this.copyTpl( 70 | $('json', 'development_package.json'), 71 | 'package.json', 72 | this.defaults 73 | ) 74 | this.copyTpl('jest.config.js', 'jest.config.js') 75 | this.copyTpl('webpack.config.js', 'webpack.config.js') 76 | this.copyTpl('license', 'LICENSE', this.defaults) 77 | this.copyTpl('readme.md', 'README.md', this.defaults) 78 | this.copyTpl($('javascripts', 'main.js'), $('src', 'main', 'index.js')) 79 | this.copyTpl( 80 | $('javascripts', 'preload.js'), 81 | $('src', 'preload', 'index.js') 82 | ) 83 | this.copyTpl('index.html', $('src', 'renderer', 'index.html')) 84 | this.copyTpl( 85 | $('stylesheets', 'application.css'), 86 | $('src', 'renderer', 'stylesheets', 'application.css') 87 | ) 88 | this.copyTpl( 89 | $('javascripts', 'renderer.js'), 90 | $('src', 'renderer', 'javascripts', 'index.js') 91 | ) 92 | this.copy($('images', 'electron.icns'), $('resources', 'icon.icns')) 93 | this.copy($('images', 'electron.ico'), $('resources', 'icon.ico')) 94 | this.copyTpl($('json', 'settings.json'), $('config', 'settings.json')) 95 | this.copyTpl( 96 | $('json', 'development.json'), 97 | $('config', 'environments', 'development.json') 98 | ) 99 | this.copyTpl( 100 | $('json', 'production.json'), 101 | $('config', 'environments', 'production.json') 102 | ) 103 | this.copyTpl( 104 | $('json', 'test.json'), 105 | $('config', 'environments', 'test.json') 106 | ) 107 | this.copyTpl( 108 | $('json', 'windows.json'), 109 | $('config', 'platforms', 'windows.json') 110 | ) 111 | this.copyTpl( 112 | $('json', 'linux.json'), 113 | $('config', 'platforms', 'linux.json') 114 | ) 115 | this.copyTpl($('json', 'mac.json'), $('config', 'platforms', 'mac.json')) 116 | this.copyTpl( 117 | $('test', 'main_test.js'), 118 | $('test', 'features', 'main.test.js') 119 | ) 120 | this.copyTpl($('test', 'setup.js'), $('test', 'setup.js'), this.defaults) 121 | } 122 | 123 | installPackages() { 124 | if (!this.options.skipInstall) { 125 | console.log(` Running ${chalk.cyan(this.defaults.packageManager + ' install')}..`) 126 | childProcess.spawnSync(this.defaults.packageManager, ['install'], { 127 | cwd: './' + this.name, 128 | shell: true, 129 | stdio: 'inherit' 130 | }) 131 | } else { 132 | console.log(` Skipping ${chalk.cyan('installing dependencies')} ..`) 133 | } 134 | } 135 | 136 | mkdir() { 137 | try { 138 | return fs.mkdirSync($.apply(this, arguments)) 139 | } catch (err) { 140 | console.log(`\n ${chalk.red(err.message)} \n`) 141 | process.exit(0) 142 | } 143 | } 144 | 145 | copy(src, dest) { 146 | const template = $(__dirname, '..', 'templates', src) 147 | const destination = $(process.cwd(), this.name, dest) 148 | fs.writeFileSync(destination, fs.readFileSync(template)) 149 | console.log(' ' + chalk.green('create') + ' ' + dest) 150 | } 151 | 152 | copyTpl(src, dest, data) { 153 | if (typeof data === 'undefined') { 154 | data = {} 155 | } 156 | const template = $(__dirname, '..', 'templates', src) 157 | const destination = $(process.cwd(), this.name, dest) 158 | const str = fs.readFileSync(template, 'utf8') 159 | 160 | fs.writeFileSync(destination, ejs.render(str, data)) 161 | console.log(' ' + chalk.green('create') + ' ' + dest) 162 | } 163 | 164 | printInstructions() { 165 | console.log('') 166 | console.log( 167 | `Success! Created ${this.name} at ${$(process.cwd(), this.name)}` 168 | ) 169 | console.log('Inside that directory, you can run several commands:') 170 | console.log('') 171 | console.log(chalk.cyan(' bozon start'.padStart(5))) 172 | console.log(' Starts the Electron app in development mode.') 173 | console.log('') 174 | console.log(chalk.cyan(' bozon test'.padStart(5))) 175 | console.log(' Starts the test runner.') 176 | console.log('') 177 | console.log(chalk.cyan(' bozon package '.padStart(5))) 178 | console.log(' Packages Electron application for specified platform.') 179 | console.log('') 180 | console.log('') 181 | console.log('We suggest you to start with typing:') 182 | console.log(chalk.cyan(` cd ${this.name}`)) 183 | console.log(chalk.cyan(' bozon start')) 184 | console.log('') 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/generator/questionnaire.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | import chalk from 'chalk' 3 | import { isBlank } from 'underscore.string' 4 | 5 | export default class Questionnaire { 6 | constructor(options) { 7 | this.name = options.name 8 | } 9 | 10 | questions() { 11 | return [ 12 | { 13 | type: 'input', 14 | name: 'name', 15 | message: 'What is the name of your app?', 16 | default: this.name, 17 | validate: value => { 18 | return isBlank(value) ? 'You have to provide application name' : true 19 | } 20 | }, { 21 | type: 'input', 22 | name: 'author', 23 | message: 'Please specify author name (ex: John Doe):', 24 | validate: value => { 25 | return isBlank(value) ? 'You have to provide author name' : true 26 | } 27 | }, { 28 | type: 'list', 29 | name: 'packageManager', 30 | message: 'Which package manager do you use?', 31 | choices: ['yarn', 'npm'] 32 | } 33 | ] 34 | } 35 | 36 | prompt(callback) { 37 | console.log(' ') 38 | console.log(' Welcome to ' + chalk.cyan('Bozon') + '!') 39 | console.log(' You\'re about to start new' + chalk.cyan(' Electron ') + 'application,') 40 | console.log(' but first answer a few questions about your project:') 41 | console.log(' ') 42 | return inquirer.prompt(this.questions()).then(answers => { 43 | callback(answers) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander' 2 | import { create, start, build, pack, test, clear } from './runner' 3 | import json from '../package.json' 4 | 5 | export const perform = () => { 6 | commander.version(json.version).usage('[options]') 7 | 8 | commander 9 | .command('new ') 10 | .option('--skip-install') 11 | .description('Generate scaffold for new Electron application') 12 | .action(create) 13 | 14 | commander 15 | .command('start') 16 | .alias('s') 17 | .option('-r, --reload') 18 | .option('-i, --inspect ') 19 | .option('-b, --inspect-brk ') 20 | .description('Compile and run application') 21 | .action(start) 22 | 23 | commander 24 | .command('build [env]') 25 | .description('Build application to builds/ directory') 26 | .action(build) 27 | 28 | commander 29 | .command('test [spec]') 30 | .description('Run tests from spec/ directory') 31 | .action(test) 32 | 33 | commander 34 | .command('clear') 35 | .description('Clear builds and releases directories') 36 | .action(clear) 37 | 38 | commander 39 | .command('package ') 40 | .option('-p, --publish') 41 | .description( 42 | 'Build and Package applications for platforms defined in package.json' 43 | ) 44 | .action(pack) 45 | 46 | commander.parse(process.argv) 47 | 48 | if (!process.argv.slice(2).length) { 49 | commander.outputHelp() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/packager/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import Checker from 'utils/checker' 3 | import { Builder } from 'builder' 4 | import { log, startSpinner, stopSpinner } from 'utils/logger' 5 | 6 | const electronBuilder = require('electron-builder') 7 | 8 | export default class Packager { 9 | constructor(platform, environment, publish) { 10 | Checker.ensure() 11 | this.platform = platform 12 | this.environment = environment 13 | this.publish = publish ? 'always' : 'never' 14 | } 15 | 16 | build() { 17 | return Builder.run(this.platform, this.environment).then(() => { 18 | startSpinner('Packaging Electron application') 19 | if (this.environment === 'test') { 20 | return this.testBuild(this.platform) 21 | } else { 22 | return this.productionBuild(this.platform, this.environment) 23 | } 24 | }) 25 | } 26 | 27 | testBuild(platform) { 28 | process.env.CSC_IDENTITY_AUTO_DISCOVERY = false 29 | return electronBuilder.build({ 30 | targets: electronBuilder.Platform[platform.toUpperCase()].createTarget(), 31 | config: { 32 | mac: { 33 | target: ['dir'] 34 | }, 35 | linux: { 36 | target: ['dir'] 37 | }, 38 | win: { 39 | target: ['dir'] 40 | }, 41 | directories: { 42 | app: path.join('builds', 'test'), 43 | buildResources: 'resources', 44 | output: '.tmp' 45 | } 46 | } 47 | }) 48 | .then(this.onSuccess) 49 | .catch(this.onError) 50 | } 51 | 52 | productionBuild(platform, environment) { 53 | return electronBuilder.build({ 54 | targets: electronBuilder.Platform[platform.toUpperCase()].createTarget(), 55 | config: { 56 | directories: { 57 | app: path.join('builds', environment), 58 | buildResources: 'resources', 59 | output: 'packages' 60 | } 61 | }, 62 | publish: this.publish 63 | }) 64 | .then(this.onSuccess) 65 | .catch(this.onError) 66 | } 67 | 68 | onSuccess() { 69 | stopSpinner('Packaging Electron application') 70 | } 71 | 72 | onError(error) { 73 | stopSpinner('Packaging Electron application', false) 74 | log(error) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/runner/index.js: -------------------------------------------------------------------------------- 1 | import Generator from 'generator' 2 | import { Starter } from 'starter' 3 | import Packager from 'packager' 4 | import { TestRunner } from 'test_runner' 5 | import { Cleaner } from 'cleaner' 6 | import { restoreCursorOnExit, platform } from 'utils' 7 | 8 | export const create = (name, command) => { 9 | const options = { skipInstall: !!command.skipInstall } 10 | new Generator(name, options).generate() 11 | } 12 | 13 | export const start = command => { 14 | restoreCursorOnExit() 15 | const params = { 16 | options: [], 17 | flags: { 18 | reload: !!command.reload 19 | } 20 | } 21 | if (command.inspect) { 22 | params.options = ['--inspect=' + command.inspect] 23 | } else if (command.inspectBrk) { 24 | params.options = ['--inspect-brk=' + command.inspectBrk] 25 | } 26 | return Starter.run(params) 27 | } 28 | 29 | export const build = (env) => { 30 | restoreCursorOnExit() 31 | return new Packager(platform(), env) 32 | .build() 33 | .then(() => process.exit(0)) 34 | .catch(() => process.exit(1)) 35 | } 36 | 37 | export const pack = (platform, command) => { 38 | restoreCursorOnExit() 39 | return new Packager(platform, 'production', !!command.publish).build() 40 | } 41 | 42 | export const test = (path) => { 43 | restoreCursorOnExit() 44 | return TestRunner.run(path) 45 | .then(() => process.exit(0)) 46 | .catch(() => process.exit(1)) 47 | } 48 | 49 | export const clear = () => { 50 | restoreCursorOnExit() 51 | return Cleaner.run() 52 | } 53 | -------------------------------------------------------------------------------- /src/starter/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { spawn } from 'child_process' 3 | import Checker from 'utils/checker' 4 | import { platform, isWindows, nodeEnv } from 'utils' 5 | import { Builder } from 'builder' 6 | import { startSpinner, stopSpinner } from 'utils/logger' 7 | 8 | const RUN_START = 'Starting application' 9 | const RUN_SUCCESS = 'Starting application' 10 | const env = process.env.NODE_CONFIG_ENV || 'development' 11 | 12 | const run = params => { 13 | Checker.ensure() 14 | Builder.run(platform(), env, params.flags) 15 | .then(() => onBuildSuccess(params)) 16 | .catch(() => {}) 17 | } 18 | 19 | const onBuildSuccess = params => { 20 | startSpinner(RUN_START) 21 | runApplication(params.options, params.flags) 22 | stopSpinner(RUN_SUCCESS) 23 | } 24 | 25 | const runApplication = (params = [], flags) => { 26 | let options 27 | 28 | if (flags.reload) { 29 | options = [ 30 | 'nodemon', 31 | `-w ${path.join('builds', env, 'main')}`, 32 | '-e js', 33 | '-q', 34 | electronPath(), 35 | path.join('builds', env) 36 | ] 37 | } else { 38 | options = ['electron', path.join('builds', env)] 39 | } 40 | spawn('npx', options.concat(params), { 41 | env: nodeEnv(env), 42 | shell: true, 43 | stdio: 'inherit' 44 | }) 45 | } 46 | 47 | const electronPath = () => { 48 | if (isWindows()) { 49 | return path.join('node_modules', 'electron', 'cli.js') 50 | } 51 | return path.join('node_modules', '.bin', 'electron') 52 | } 53 | 54 | export const Starter = { run } 55 | -------------------------------------------------------------------------------- /src/test_runner/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { spawnSync } from 'child_process' 3 | import Packager from 'packager' 4 | import Checker from 'utils/checker' 5 | import { platform, nodeEnv } from 'utils' 6 | import { log } from 'utils/logger' 7 | 8 | const run = path => { 9 | Checker.ensure() 10 | if (!path || path.match(/features/)) { 11 | return buildAndRun(path) 12 | } else { 13 | log(chalk.bold('Running test suite...')) 14 | return runUnitTests(path) 15 | } 16 | } 17 | 18 | const buildAndRun = path => { 19 | return new Packager(platform(), 'test').build().then(() => { 20 | log(chalk.bold('Running test suite...')) 21 | if (!path) { 22 | return runAllTests() 23 | } 24 | return runFeatureTests(path) 25 | }) 26 | } 27 | 28 | const runFeatureTests = path => { 29 | const result = spawnSync('npx', ['jest', '-i', path], { 30 | env: nodeEnv('test'), 31 | shell: true, 32 | stdio: 'inherit' 33 | }) 34 | return buildPromise(result.status) 35 | } 36 | 37 | const runUnitTests = path => { 38 | const result = spawnSync('npx', ['jest', path], { 39 | env: nodeEnv('test'), 40 | shell: true, 41 | stdio: 'inherit' 42 | }) 43 | return buildPromise(result.status) 44 | } 45 | 46 | const runAllTests = () => { 47 | return Promise.all([ 48 | runUnitTests('./test/units'), 49 | runFeatureTests('./test/features') 50 | ]) 51 | } 52 | 53 | const buildPromise = status => { 54 | return (status === 0) 55 | ? Promise.resolve() 56 | : Promise.reject(Error('Some tests failed')) 57 | } 58 | 59 | export const TestRunner = { run } 60 | -------------------------------------------------------------------------------- /src/utils/__mocks__/checker.js: -------------------------------------------------------------------------------- 1 | const Checker = { 2 | ensure: jest.fn() 3 | } 4 | 5 | export default Checker 6 | -------------------------------------------------------------------------------- /src/utils/__mocks__/logger.js: -------------------------------------------------------------------------------- 1 | export const log = jest.fn() 2 | export const startSpinner = jest.fn() 3 | export const stopSpinner = jest.fn() 4 | 5 | export default { log, startSpinner, stopSpinner } 6 | -------------------------------------------------------------------------------- /src/utils/checker.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import chalk from 'chalk' 4 | 5 | const ensureFilesPresence = () => { 6 | ['package.json'].forEach((file) => { 7 | try { 8 | fs.lstatSync(path.join(process.cwd(), file)) 9 | } catch (e) { 10 | log('\n Could not find ' + 11 | chalk.yellow(file) + 12 | ".. It doesn't look like you are in electron app root directory.\n") 13 | } 14 | }) 15 | } 16 | 17 | const ensureDependencies = () => { 18 | try { 19 | fs.lstatSync(path.join(process.cwd(), 'node_modules')) 20 | } catch (e) { 21 | log('\n Run ' + chalk.cyan('npm install') + '.. \n') 22 | } 23 | } 24 | 25 | const log = (error) => { 26 | console.log(error) 27 | process.exit() 28 | } 29 | 30 | const ensure = () => { 31 | ensureFilesPresence() 32 | ensureDependencies() 33 | } 34 | 35 | export default { ensure } 36 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import Config from 'merge-config' 3 | 4 | const srcDir = 'src' 5 | 6 | export const source = function () { 7 | const prefix = process.cwd() 8 | const suffix = path.join.apply(null, arguments) 9 | return path.join(prefix, suffix) 10 | } 11 | 12 | export const sourcePath = (suffix) => { 13 | if (suffix == null) { 14 | suffix = '' 15 | } 16 | return path.join(process.cwd(), srcDir, suffix) 17 | } 18 | 19 | export const destinationPath = (suffix, env) => { 20 | if (suffix == null) { 21 | suffix = '' 22 | } 23 | return path.join(process.cwd(), 'builds', env, suffix) 24 | } 25 | 26 | export const isWindows = () => { 27 | const { platform } = process 28 | return platform === 'windows' || platform === 'win32' 29 | } 30 | 31 | export const isMacOS = () => { 32 | const { platform } = process 33 | return platform === 'mac' || platform === 'darwin' 34 | } 35 | 36 | export const isLinux = () => { 37 | const { platform } = process 38 | return platform === 'linux' 39 | } 40 | 41 | export const platform = () => { 42 | if (isMacOS()) { 43 | return 'mac' 44 | } else if (isWindows()) { 45 | return 'windows' 46 | } else if (isLinux()) { 47 | return 'linux' 48 | } else { 49 | throw new Error('Unsupported platform ' + process.platform) 50 | } 51 | } 52 | 53 | export const config = (env, platform) => { 54 | const config = new Config() 55 | config.file(source('config', 'settings.json')) 56 | config.file(source('config', 'environments', env + '.json')) 57 | config.file(source('config', 'platforms', platform + '.json')) 58 | return config.get() 59 | } 60 | 61 | // Put back cursor to console on exit 62 | export const restoreCursorOnExit = () => { 63 | process.on('SIGINT', () => process.exit()) 64 | process.on('exit', () => console.log('\x1B[?25h')) 65 | } 66 | 67 | export const nodeEnv = (value) => { 68 | const env = Object.create(process.env) 69 | env.NODE_ENV = value 70 | return env 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { Spinner } from 'utils/spinner' 3 | 4 | let spinner = null 5 | 6 | const formatMessage = (message, options = {}) => { 7 | let string = `[${chalk.cyan('bozon')}] ${message}` 8 | if (options.newLineBefore) { 9 | string = `\n${string}` 10 | } 11 | if (!options.skipLineAfter) { 12 | string = `${string}\n` 13 | } 14 | return string 15 | } 16 | 17 | const log = (message, options = {}) => { 18 | process.stdout.write(formatMessage(message, options)) 19 | } 20 | 21 | const warn = (message) => { 22 | log(chalk.yellow(`⚠ ${message}`)) 23 | } 24 | 25 | const startSpinner = (message) => { 26 | spinner = new Spinner() 27 | spinner.start(formatMessage(chalk.bold(message), { skipLineAfter: true })) 28 | } 29 | 30 | const stopSpinner = (message, success = true) => { 31 | if (success) { 32 | message = `${chalk.bold(message)} ${chalk.green('✓')}` 33 | } else { 34 | message = `${chalk.yellow(message)} ${chalk.red('✖')}` 35 | } 36 | spinner.stop(formatMessage(message)) 37 | } 38 | 39 | export { log, warn, startSpinner, stopSpinner } 40 | -------------------------------------------------------------------------------- /src/utils/spinner.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import readline from 'readline' 3 | 4 | const FRAMES = [ 5 | '⣾', 6 | '⣽', 7 | '⣻', 8 | '⢿', 9 | '⡿', 10 | '⣟', 11 | '⣯', 12 | '⣷' 13 | ] 14 | const INTERVAL = 50 15 | 16 | export class Spinner { 17 | start(message) { 18 | process.stdout.write('\x1B[?25l') 19 | let i = 0 20 | this.interval = setInterval(() => { 21 | const frame = FRAMES[i] 22 | process.stdout.write(`${message} ${chalk.cyan(frame)}`) 23 | readline.cursorTo(process.stdout, 0) 24 | i = i === FRAMES.length - 1 ? 0 : i + 1 25 | }, INTERVAL) 26 | } 27 | 28 | stop(message) { 29 | clearInterval(this.interval) 30 | readline.clearLine(process.stdout) 31 | process.stdout.write(`${message}`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/gitignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | node_modules 3 | builds 4 | packages 5 | bower_components 6 | -------------------------------------------------------------------------------- /templates/images/electron.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsware/bozon/3ee36fb7e17fda8d18a5f561a42c0c23691d4d8d/templates/images/electron.icns -------------------------------------------------------------------------------- /templates/images/electron.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsware/bozon/3ee36fb7e17fda8d18a5f561a42c0c23691d4d8d/templates/images/electron.ico -------------------------------------------------------------------------------- /templates/images/electron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsware/bozon/3ee36fb7e17fda8d18a5f561a42c0c23691d4d8d/templates/images/electron.png -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

12 |
13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/javascripts/main.js: -------------------------------------------------------------------------------- 1 | // In this file you can include the rest of your app's specific main process 2 | // code. You can also put them in separate files and require them here. 3 | import path from 'path' 4 | import { app, BrowserWindow } from 'electron' 5 | 6 | const createWindow = () => { 7 | // Create the browser window. 8 | let win = new BrowserWindow({ 9 | title: CONFIG.name, 10 | width: CONFIG.width, 11 | height: CONFIG.height, 12 | webPreferences: { 13 | worldSafeExecuteJavaScript: true, 14 | preload: path.join(app.getAppPath(), 'preload', 'index.js') 15 | } 16 | }) 17 | 18 | // and load the index.html of the app. 19 | win.loadFile('renderer/index.html') 20 | 21 | // send data to renderer process 22 | win.webContents.on('did-finish-load', () => { 23 | win.webContents.send('loaded', { 24 | appName: CONFIG.name, 25 | electronVersion: process.versions.electron, 26 | nodeVersion: process.versions.node, 27 | chromiumVersion: process.versions.chrome 28 | }) 29 | }) 30 | 31 | win.on('closed', () => { 32 | win = null 33 | }) 34 | } 35 | 36 | // This method will be called when Electron has finished 37 | // initialization and is ready to create browser windows. 38 | // Some APIs can only be used after this event occurs. 39 | app.whenReady().then(createWindow) 40 | 41 | // Quit when all windows are closed. 42 | app.on('window-all-closed', () => { 43 | // On macOS it is common for applications and their menu bar 44 | // to stay active until the user quits explicitly with Cmd + Q 45 | if (process.platform !== 'darwin') { 46 | app.quit() 47 | } 48 | }) 49 | 50 | app.on('activate', () => { 51 | // On macOS it's common to re-create a window in the app when the 52 | // dock icon is clicked and there are no other windows open. 53 | if (BrowserWindow.getAllWindows().length === 0) { 54 | createWindow() 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /templates/javascripts/preload.js: -------------------------------------------------------------------------------- 1 | // https://electronjs.org/docs/tutorial/security 2 | // Preload File that should be loaded into browser window instead of 3 | // setting nodeIntegration: true for browser window 4 | 5 | import { contextBridge, ipcRenderer } from 'electron' 6 | 7 | contextBridge.exposeInMainWorld('MessagesAPI', { 8 | onLoaded: callback => { 9 | ipcRenderer.on('loaded', callback) 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /templates/javascripts/renderer.js: -------------------------------------------------------------------------------- 1 | require('application.css') 2 | 3 | window.MessagesAPI.onLoaded((_, data) => { 4 | document.getElementById('title').innerHTML = data.appName + ' App' 5 | document.getElementById('details').innerHTML = 'built with Electron v' + data.electronVersion 6 | document.getElementById('versions').innerHTML = 'running on Node v' + data.nodeVersion + ' and Chromium v' + data.chromiumVersion 7 | }) 8 | -------------------------------------------------------------------------------- /templates/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/z_/3ndgqq0n609bqbnfhw41z2fr0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: undefined, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: undefined, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: undefined, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: undefined, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: undefined, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: undefined, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | modulePathIgnorePatterns: ['/builds'], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: undefined, 95 | 96 | // Run tests from one or more projects 97 | // projects: undefined, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: undefined, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: undefined, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | setupFiles: ['/test/setup.js'], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | // setupFilesAfterEnv: [], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: 'node' 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "/node_modules/" 152 | // ], 153 | 154 | // The regexp pattern or array of patterns that Jest uses to detect test files 155 | // testRegex: [], 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: undefined, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | // transform: undefined, 171 | 172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 173 | // transformIgnorePatterns: [ 174 | // "/node_modules/" 175 | // ], 176 | 177 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 178 | // unmockedModulePathPatterns: undefined, 179 | 180 | // Indicates whether each individual test should be reported during the run 181 | // verbose: undefined, 182 | 183 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 184 | // watchPathIgnorePatterns: [], 185 | 186 | // Whether to use watchman for file crawling 187 | // watchman: true, 188 | }; 189 | -------------------------------------------------------------------------------- /templates/json/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "width": 680, 3 | "reload": true, 4 | "devTools": true 5 | } 6 | -------------------------------------------------------------------------------- /templates/json/development_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "version": "0.1.0", 4 | "description": "<%= name %> application build with Electron", 5 | "author": { 6 | "name": "<%= author %>", 7 | "email": "" 8 | }, 9 | "repository": {}, 10 | "dependencies": {}, 11 | "license": "ISC", 12 | "devDependencies": { 13 | "bozon": "<%= bozonVersion %>", 14 | "jest": "<%= jestVersion %>", 15 | "spectron": "<%= spectronVersion %>" 16 | }, 17 | "build": { 18 | "appId": "com.electron.<%= id %>", 19 | "win": { 20 | "publish": null 21 | }, 22 | "mac": { 23 | "publish": null, 24 | "category": "your.app.category.type" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/json/linux.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": ".png" 3 | } 4 | -------------------------------------------------------------------------------- /templates/json/mac.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": ".png" 3 | } 4 | -------------------------------------------------------------------------------- /templates/json/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "reload": false, 3 | "devTools": false 4 | } 5 | -------------------------------------------------------------------------------- /templates/json/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "width": 600, 3 | "height": 400 4 | } 5 | -------------------------------------------------------------------------------- /templates/json/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "reload": false, 3 | "devTools": false 4 | } 5 | -------------------------------------------------------------------------------- /templates/json/windows.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": ".ico" 3 | } 4 | -------------------------------------------------------------------------------- /templates/license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) <%= author%> <%= year %> 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/readme.md: -------------------------------------------------------------------------------- 1 | # <%= name %> 2 | 3 | > My <%= name%> app built with Electron 4 | 5 | 6 | ## Dev 7 | 8 | ``` 9 | $ npm install 10 | ``` 11 | 12 | ### Run 13 | 14 | ``` 15 | $ bozon start 16 | ``` 17 | 18 | ### Package 19 | 20 | ``` 21 | $ bozon package 22 | ``` 23 | 24 | Builds the app for OS X, Linux, and Windows, using [electron-builder](https://github.com/electron-userland/electron-builder). 25 | 26 | 27 | ## License 28 | 29 | The MIT License (MIT) © <%= author%> <%= year %> 30 | -------------------------------------------------------------------------------- /templates/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding: 0; 3 | margin: 0; 4 | height: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | font-family: -apple-system, "Helvetica Neue", Helvetica, sans-serif; 10 | } 11 | 12 | .message { 13 | position: absolute; 14 | width: 500px; 15 | height: 250px; 16 | top: 50%; 17 | left: 50%; 18 | color: #777; 19 | font-weight: 200; 20 | text-align: center; 21 | margin-top: -125px; 22 | margin-left: -250px; 23 | } 24 | 25 | .message h1 { 26 | font-size: 50px; 27 | font-weight: 100; 28 | color: #333; 29 | } 30 | 31 | .message div { 32 | margin-bottom: 10px; 33 | } 34 | -------------------------------------------------------------------------------- /templates/test/main_test.js: -------------------------------------------------------------------------------- 1 | describe('application launch', () => { 2 | beforeEach(() => app.start()) 3 | 4 | afterEach(() => { 5 | if (app && app.isRunning()) { 6 | return app.stop() 7 | } 8 | }) 9 | 10 | test('shows an initial window', async () => { 11 | const count = await app.client.getWindowCount() 12 | expect(count).toBe(1) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /templates/test/setup.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { Application } = require('spectron') 3 | 4 | const appPath = () => { 5 | switch (process.platform) { 6 | case 'darwin': 7 | return path.join(__dirname, '..', '.tmp', 'mac', '<%= name%>.app', 'Contents', 'MacOS', '<%= name%>') 8 | case 'linux': 9 | return path.join(__dirname, '..', '.tmp', 'linux', '<%= name%>') 10 | case 'win32': 11 | return path.join(__dirname, '..', '.tmp', 'win-unpacked', '<%= name%>.exe') 12 | default: 13 | throw Error(`Unsupported platform ${process.platform}`) 14 | } 15 | } 16 | global.app = new Application({ path: appPath() }) 17 | -------------------------------------------------------------------------------- /templates/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | renderer: { 3 | entry: './src/renderer/javascripts/index.js' 4 | }, 5 | preload: { 6 | entry: './src/preload/index.js' 7 | }, 8 | main: { 9 | entry: './src/main/index.js' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const nodeExternals = require('webpack-node-externals') 3 | 4 | module.exports = { 5 | entry: { 6 | index: path.resolve(__dirname, 'src', 'index.js'), 7 | dev: path.resolve(__dirname, 'src', 'dev', 'index.js') 8 | }, 9 | mode: 'development', 10 | target: 'node', 11 | node: { 12 | __dirname: false, 13 | __filename: false 14 | }, 15 | externals: [nodeExternals()], 16 | output: { 17 | path: path.resolve(__dirname, 'dist'), 18 | libraryTarget: 'commonjs2' 19 | }, 20 | resolve: { 21 | modules: ['node_modules', 'src'] 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | exclude: /(node_modules)/, 28 | use: { 29 | loader: 'babel-loader', 30 | options: { 31 | presets: [ 32 | [ 33 | '@babel/preset-env', 34 | { 35 | targets: { 36 | esmodules: true 37 | } 38 | } 39 | ] 40 | ] 41 | } 42 | } 43 | } 44 | ] 45 | } 46 | } 47 | --------------------------------------------------------------------------------