├── .nvmrc
├── templates
├── json
│ ├── linux.json
│ ├── mac.json
│ ├── windows.json
│ ├── settings.json
│ ├── test.json
│ ├── production.json
│ ├── development.json
│ └── development_package.json
├── gitignore
├── images
│ ├── electron.icns
│ ├── electron.ico
│ └── electron.png
├── webpack.config.js
├── test
│ ├── main_test.js
│ └── setup.js
├── javascripts
│ ├── renderer.js
│ ├── preload.js
│ └── main.js
├── index.html
├── readme.md
├── stylesheets
│ └── application.css
├── license
└── jest.config.js
├── .gitignore
├── src
├── __mocks__
│ ├── cleaner.js
│ ├── starter.js
│ ├── generator.js
│ ├── packager.js
│ ├── builder.js
│ ├── test_runner.js
│ └── utils.js
├── utils
│ ├── __mocks__
│ │ ├── checker.js
│ │ └── logger.js
│ ├── spinner.js
│ ├── checker.js
│ ├── logger.js
│ └── index.js
├── generator
│ ├── __mocks__
│ │ └── questionnaire.js
│ ├── questionnaire.js
│ └── index.js
├── builder
│ ├── messages.js
│ ├── bundle.js
│ ├── webpack_config
│ │ ├── __mocks__
│ │ │ └── index.js
│ │ ├── defaults.js
│ │ └── index.js
│ ├── manifest.js
│ ├── html.js
│ ├── index.js
│ └── watcher.js
├── cleaner
│ └── index.js
├── dev
│ └── index.js
├── index.js
├── runner
│ └── index.js
├── starter
│ └── index.js
├── test_runner
│ └── index.js
└── packager
│ └── index.js
├── __mocks__
├── chokidar.js
├── latest-version.js
├── ejs.js
├── inquirer.js
├── child_process.js
├── fs-extra.js
├── webpack.js
├── merge-config.js
├── chalk.js
├── electron-builder.js
├── commander.js
└── fs.js
├── bin
└── bozon
├── .prettierrc.js
├── __test__
├── setup.js
├── utils
│ ├── checker.test.js
│ └── index.test.js
├── cleaner
│ └── index.test.js
├── packager
│ └── index.test.js
├── generator
│ ├── questionnaire.test.js
│ └── index.test.js
├── builder
│ └── index.test.js
├── starter
│ └── index.test.js
├── test_runner
│ └── index.test.js
├── runner
│ └── index.test.js
└── index.test.js
├── babel.config.js
├── .npmignore
├── .editorconfig
├── .eslintrc.js
├── .github
└── workflows
│ └── ci.yml
├── webpack.config.js
├── LICENSE
├── CONTRIBUTING.md
├── package.json
├── README.md
└── jest.config.js
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16.10.0
2 |
--------------------------------------------------------------------------------
/templates/json/linux.json:
--------------------------------------------------------------------------------
1 | {
2 | "icon": ".png"
3 | }
4 |
--------------------------------------------------------------------------------
/templates/json/mac.json:
--------------------------------------------------------------------------------
1 | {
2 | "icon": ".png"
3 | }
4 |
--------------------------------------------------------------------------------
/templates/json/windows.json:
--------------------------------------------------------------------------------
1 | {
2 | "icon": ".ico"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn-error.log
3 | dist
4 | .vscode
5 |
--------------------------------------------------------------------------------
/src/__mocks__/cleaner.js:
--------------------------------------------------------------------------------
1 | export const Cleaner = { run: jest.fn() }
2 |
--------------------------------------------------------------------------------
/src/__mocks__/starter.js:
--------------------------------------------------------------------------------
1 | export const Starter = { run: jest.fn() }
2 |
--------------------------------------------------------------------------------
/__mocks__/chokidar.js:
--------------------------------------------------------------------------------
1 | const watch = jest.fn()
2 | export default { watch }
3 |
--------------------------------------------------------------------------------
/__mocks__/latest-version.js:
--------------------------------------------------------------------------------
1 | export default jest.fn().mockResolvedValue('1.0.0')
2 |
--------------------------------------------------------------------------------
/bin/bozon:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | require('../dist/index.js').perform()
4 |
--------------------------------------------------------------------------------
/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/gitignore:
--------------------------------------------------------------------------------
1 | .tmp
2 | node_modules
3 | builds
4 | packages
5 | bower_components
6 |
--------------------------------------------------------------------------------
/templates/json/production.json:
--------------------------------------------------------------------------------
1 | {
2 | "reload": false,
3 | "devTools": false
4 | }
5 |
--------------------------------------------------------------------------------
/__mocks__/ejs.js:
--------------------------------------------------------------------------------
1 | const render = jest.fn(value => value)
2 |
3 | export default { render }
4 |
--------------------------------------------------------------------------------
/templates/json/development.json:
--------------------------------------------------------------------------------
1 | {
2 | "width": 680,
3 | "reload": true,
4 | "devTools": true
5 | }
6 |
--------------------------------------------------------------------------------
/templates/images/electron.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/railsware/bozon/HEAD/templates/images/electron.icns
--------------------------------------------------------------------------------
/templates/images/electron.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/railsware/bozon/HEAD/templates/images/electron.ico
--------------------------------------------------------------------------------
/templates/images/electron.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/railsware/bozon/HEAD/templates/images/electron.png
--------------------------------------------------------------------------------
/src/utils/__mocks__/checker.js:
--------------------------------------------------------------------------------
1 | const Checker = {
2 | ensure: jest.fn()
3 | }
4 |
5 | export default Checker
6 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'none',
3 | tabWidth: 2,
4 | semi: false,
5 | singleQuote: true,
6 | }
7 |
--------------------------------------------------------------------------------
/__mocks__/inquirer.js:
--------------------------------------------------------------------------------
1 | const prompt = jest.fn().mockResolvedValue({
2 | name: 'myapp', author: 'John Doe'
3 | })
4 |
5 | export default { prompt }
6 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/__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__/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/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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/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/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/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/__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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/__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__/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__/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bozon
2 | > Command line tool for building, testing and publishing modern [Electron](http://electron.atom.io/) applications
3 |
4 | [](https://badge.fury.io/js/bozon)
5 | [](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 | 
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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------