This project is using Electron to create an Angular app using Jest instead of Karma, Cypress instead of Protractor, Angular Material and TailwindCSS. Moreover, formatting, style linting, documentation generation and git hooks (for linting) have been added to be more complete. This README explains all modifications made to initial Angular project.
This project is fully inspired by angular-electron by Maxime Gris, but with addition of TailwindCSS, Jest, Material, Stylelint, Compodoc and several tools.
Before you begin we recommend you read about the basic building blocks that assemble this application:
Make sure you have installed all of the following prerequisites on your development machine:
$ git clone https://github.com/gael-clair/angular-electron.git YOUR_REPO_FOLDER
$ cd YOUR_REPO_FOLDER
$ git remote remove origin
$ git remote add origin YOUR_REPO_URL
Update project name in package.json
Install dependencies
$ npm install
# build application in production mode
$ yarn build
# build angular app
$ yarn ng:tw:build
# build angular app in production mode
$ yarn ng:tw:build:prod
# build electron app
$ yarn electron:build
# build electron app in production mode
$ yarn electron:build:prod
# starts angular development server with live reload and open electron in watch mode
$ yarn start
# starts angular and electron unit tests
$ yarn test
# starts angular and electron unit tests with source/test files watch
$ yarn test:watch
# starts angular and electron unit tests in CI mode with coverage (coverage in coverage folder and reports in reports folder)
$ yarn test:ci
# starts angular unit tests
$ yarn ng:test
# starts angular unit tests with source/test files watch
$ yarn ng:test:watch
# starts angular unit tests with coverage
$ yarn ng:test:cov
# starts angular unit tests in CI mode with coverage (coverage in coverage folder and reports in reports folder)
$ yarn ng:test:ci
# starts electron unit tests
$ yarn electron:test
# starts electron unit tests with source/test files watch
$ yarn electron:test:watch
# starts electron unit tests with coverage
$ yarn electron:test:cov
# starts electron unit tests in CI mode with coverage (coverage in coverage folder and reports in reports folder)
$ yarn electron:test:ci
# starts angular and open cypress
$ yarn ng:e2e
# starts angular and run cypress
$ yarn ng:e2e:ci
# builds angular and electron app and starts jest tests with spectron
$ yarn e2e
# builds angular and electron app and starts jest tests in CI mode with spectron
$ yarn e2e:ci
# lints source and test files
$ yarn lint
# lints source and test files and fixes errors if possible
$ yarn lint:fix
# lints source and test files and generate a report (report at reports/lint.xml)
$ yarn lint:ci
# lints style files
$ yarn lint:style
# lints style files and fixes errors if possible
$ yarn lint:style:fix
# lints style files and generate a report (report at reports/style.xml)
$ yarn lint:style:ci
# formats source and test files
$ yarn format
# generates angular project documentation
$ yarn ng:doc
The repo includes a Github Action configuration for continuous integration (workflow file .github/workflows/checks.yml
) It is also integrated with Coveralls during workflow process.
To let electron-builder publish new release, you have to had to repository secrets a Github token with write:package permission. When token generated go to Repo Settings > Secrets > New secret
add a secret with GH_TOKEN name and token as value.
It will be automatically exposed as en environment variable in the workflow.
To be sure to pass checks before merging a Pull Request you should add required checks in Repo Settings > Branches > Branch protection rules > Require status checks to pass before merging
:
To add a check failure if coverage percentage is under a certain threshold, go to the repository page on Coveralls and go to Settings > PULL REQUESTS ALERTS > COVERAGE THRESHOLD FOR FAILURE
and set minimal value for coverage percentage (for example 80%).
This project is based on Angular. The @angular/cli command used to generate this project is:
$ npx @angular/cli new angular-electron --routing=true --style=scss --minimal=true
Rename tsconfig.json
to tsconfig.base.json
.
Move tsconfig.app.json
and tsconfig.spec.json
to src
folder.
Update tsconfig.base.json
with:
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": ".",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"esModuleInterop": true, // add
"allowSyntheticDefaultImports": true, // add
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"module": "es2020",
"lib": ["es2018", "dom"]
}
}
src/tsconfig.app.json
with:{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app"
},
"files": [
"main.ts", // remove src/
"polyfills.ts" // remove src/
],
"include": [
"**/*.d.ts" // remove src/
]
}
src/tsconfig.spec.json
with:{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jasmine"]
},
"files": [
"test.ts", // remove src/
"polyfills.ts" // remove src/
],
"include": [
"**/*.spec.ts", // remove src/
"**/*.d.ts" // remove src/
]
}
src/tsconfig.json
with:{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"paths": {
"@app/core/*": ["src/app/core/*"],
"@app/core": ["src/app/core"],
"@app/shared/*": ["src/app/shared/*"],
"@app/shared": ["src/app/shared"],
"@app/features/*": ["src/app/features/*"],
"@app/features": ["src/app/features"],
"@app/env": ["src/environments/environment"],
"@app/types": ["src/app/types"],
"@test/utils": ["src/test.utils.ts"]
}
}
}
angular.json > projects > angular-electron > architect > build > options
with:{
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "src/dist", // update
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json", // add src/
"aot": true,
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
}
}
}
angular.json > projects > angular-electron > architect > lint > options > tsConfig
with:{
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json", // add src/
"src/tsconfig.spec.json" // add src/
],
"exclude": ["**/node_modules/**"]
}
}
}
TailwindCSS is used with Material to provide UI components and utilities.
$ yarn add -D tailwindcss ng-tailwindcss
tailwind.config.js
with:$ npx tailwind init
src/tailwind.css
with:@tailwind base;
@tailwind components;
@tailwind utilities;
ng-tailwind.js
file with:$ npx ngtw configure
ng-tailwind.js
to be compatible with windows and linux:module.exports = {
// Tailwind Paths
configJS: 'tailwind.config.js',
sourceCSS: 'src/tailwind.css', // if created on windows, replace \\ with /
outputCSS: 'src/styles.css', // if created on windows, replace \\ with /
// Sass
sass: false,
// PurgeCSS Settings
purge: false,
keyframes: false,
fontFace: false,
rejected: false,
whitelist: [],
whitelistPatterns: [],
whitelistPatternsChildren: [],
extensions: ['.ts', '.html', '.js'],
extractors: [],
content: [],
};
angular.json > projects > angular-electron > architect > build
:{
"build": {
"options": {
"styles": ["src/styles.scss", "src/styles.css"] // add styles.css file a source file for styles
}
}
}
src/styles.css
before using app (serving, building or testing) be sure to run:# build Tailwind CSS
$ ngtw build
# build Tailwind CSS with purge of unused elements
$ ngtw build --purge
src/styles.css
when source files are modified, use watch mode in parallel to serve mode:$ ngtw watch
src/styles.css
to .gitignore
file.This project is using Material Design with @angular/material installed with:
$ yarn add @angular/cdk hammerjs
$ yarn ng add @angular/material
# Theme: Purple/Green
# No global Angular Material typography styles
# With browser animations for Angular Material
angular.json > projects > angular-electron > architect > build > options > styles
:{
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css", // add material style
"src/styles.scss",
"src/styles.css"
]
}
Unit testing is based initially on the use of Karma/Jasmine but this project uses Jest instead. To add Jest:
$ yarn add -D jest jest-preset-angular jest-junit ts-jest @types/jest jest-html-reporter
types
property and test.ts
from files
property from tsconfig.spec.json
:{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec"
},
"files": ["polyfills.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
}
src/jest.config.js
with:const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { compilerOptions } = require('./tsconfig.json');
module.exports = {
preset: 'jest-preset-angular',
verbose: true,
setupFilesAfterEnv: ['./setupJest.ts'],
coverageDirectory: '../coverage/angular/ut',
collectCoverageFrom: ['app/**/*.ts', '!app/**/*(index|*.module|*.routes|index.d).ts'],
globals: {
'ts-jest': {
tsConfig: 'src/tsconfig.spec.json',
},
},
reporters: [
'default',
[
'jest-junit',
{
suiteName: 'Unit tests',
outputDirectory: 'reports/angular',
outputName: 'ut.xml',
},
],
[
'../node_modules/jest-html-reporter',
{
pageTitle: 'Unit tests Report',
outputPath: 'reports/angular/ut.html',
includeFailureMsg: true,
},
],
],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, {
prefix: '<rootDir>/..',
}),
};
src/setupJest.ts
with:import 'jest-preset-angular';
const mock = () => {
let storage: { [key: string]: string } = {};
return {
getItem: (key: string) => (key in storage ? storage[key] : null),
setItem: (key: string, value: string) => (storage[key] = value || ''),
removeItem: (key: string) => delete storage[key],
clear: () => (storage = {}),
};
};
Object.defineProperty(window, 'localStorage', { value: mock() });
Object.defineProperty(window, 'sessionStorage', { value: mock() });
Object.defineProperty(window, 'CSS', { value: null });
Object.defineProperty(window, 'getComputedStyle', {
value: () => {
return {
display: 'none',
appearance: ['-webkit-appearance'],
};
},
});
Object.defineProperty(document, 'doctype', {
value: '<!DOCTYPE html>',
});
Object.defineProperty(document.body.style, 'transform', {
value: () => {
return {
enumerable: true,
configurable: true,
};
},
});
Remove Angular test target in angular.json > projects > angular-electron > architect > test
If you generated the project without --minimal=true
:
$ rm karma.conf.js src/test.ts
$ yarn remove karma karma-chrome-launcher karma-coverage-istanbul-reporter karma-jasmine karma-jasmine-html-reporter
$ yarn add -D @briebug/cypress-schematic wait-on
$ yarn ng add @briebug/cypress-schematic --noBuilder
# Remove Protractor
cypress/tsconfig.json
with:{
"extends": "../tsconfig.base.json", // refer to base json
"compilerOptions": {
"outDir": "../out-tsc/cypress", // change
"sourceMap": false
},
"include": ["../node_modules/cypress", "**/*.ts"]
}
angular.json > projects > angular-electron > architect > lint
switching typescript config file for e2e from e2e/tsconfig.json
to cypress/tsconfig.json
:{
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json", "cypress/tsconfig.json"], // replace e2e/ with cypress/
"exclude": ["**/node_modules/**"]
}
}
}
To add coverage to cypress:
$ yarn add -D ngx-build-plus istanbul-instrumenter-loader @istanbuljs/nyc-config-typescript source-map-support ts-node @cypress/code-coverage nyc istanbul-lib-coverage
cypress/coverage.webpack.js
with:module.exports = {
module: {
rules: [
{
test: /\.(js|ts)$/,
loader: 'istanbul-instrumenter-loader',
options: { esModules: true },
enforce: 'post',
include: require('path').join(__dirname, '../src/app'),
exclude: [/\.(e2e|spec)\.ts$/, /node_modules/, /(ngfactory|ngstyle)\.js/],
},
],
},
};
angular.json > projects > angular-electron > architect > e2e
with:{
"e2e": {
"builder": "ngx-build-plus:dev-server",
"options": {
"browserTarget": "angular-electron:build",
"extraWebpackConfig": "./cypress/coverage.webpack.js"
}
}
}
.nycrc.json
with:{
"extends": "@istanbuljs/nyc-config-typescript",
"report-dir": "./coverage/angular/ti"
}
cypress/support/index.d.ts
:import '@cypress/code-coverage/support';
cypress/plugins/index.js
with:const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');
const registerCodeCoverageTasks = require('@cypress/code-coverage/task');
module.exports = (on, config) => {
on('file:preprocessor', cypressTypeScriptPreprocessor);
return registerCodeCoverageTasks(on, config); // activate coverage task
};
yarn e2e
use:$ cypress run
If you generated project without --minimal=true
you could delete some file and configuration:
$ yarn remove jasmine-core jasmine-spec-reporter @types/jasmine @types/jasminewd2
$ yarn add -D tslint-angular
tslint.json
:{
"extends": ["tslint:recommended", "tslint-angular"]
}
$ yarn add -D prettier tslint-config-prettier tslint-plugin-prettier
.prettierrc.json
with:{
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 120,
"tabWidth": 2
}
tslint-plugin-prettier
to rulesDirectory in tslint.json
:{
"extends": ["tslint:recommended", "tslint-angular", "tslint-config-prettier"],
"rulesDirectory": ["codelyzer", "tslint-plugin-prettier"] // add rules
"rules": {
"prettier": true // apply prettier rules
}
}
.prettierignore
with:/.nyc_output
/coverage
/electron/dist
/reports
/src/styles.css
/src/dist
In order to add linting of style files (css and scss), Stylelint is used with some rules presets.
$ yarn add -D stylelint stylelint-config-recommended stylelint-junit-formatter stylelint-no-unsupported-browser-features stylelint-config-prettier stylelint-prettier make-dir
.stylelintrc.json
file with:{
"extends": ["stylelint-prettier/recommended", "stylelint-config-recommended", "stylelint-config-prettier"],
"plugins": ["stylelint-no-unsupported-browser-features", "stylelint-prettier"],
"rules": {
"prettier/prettier": true,
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": ["tailwind"]
}
],
"plugin/no-unsupported-browser-features": [
true,
{
"severity": "warning"
}
],
"no-empty-source": null
},
"junit-formatter": {
"outputPath": "reports/style.xml"
}
}
One pre-commit git hook is activated with Husky to call Lint-staged to format and lint files to be commited. If one operation fails commit is canceled. One commit-msg git hook is set to let @commitlint/cli lint commit message to ensure that it follows conventional-changelog format.
To configure git hooks you have to:
yarn add -D husky lint-staged @commitlint/cli @commitlint/config-conventional cz-conventional-changelog commitizen
.cz.json
with:{
"path": "cz-conventional-changelog"
}
.commitlintrc.json
with:{
"extends": ["@commitlint/config-conventional"]
}
.huskyrc.json
with:{
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
}
.lintstagedrc
with JSON configuration:{
"*.{json,js,ts,tsx,md,html,yml}": "prettier --write",
"*.{scss,css}": "stylelint --fix",
"*.{ts,tsx,js,jsx}": "tslint --fix"
}
Compodoc is used to write documentation.
$ yarn add -D @compodoc/compodoc
$ yarn add -D electron
Create folder electron/src
.
Create file electron/tsconfig.json
with:
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"module": "commonjs"
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.spec.ts"]
}
electron/main.ts
with:import { app, BrowserWindow, screen } from 'electron';
import * as path from 'path';
import * as url from 'url';
let win: BrowserWindow = null;
const args = process.argv.slice(1);
const serve = args.some((val) => val === '--serve');
function createWindow(): BrowserWindow {
const size = screen.getPrimaryDisplay().workAreaSize;
// Create the browser window.
win = new BrowserWindow({
center: true,
width: size.width / 2,
height: size.height / 2,
webPreferences: {
nodeIntegration: false, // disabled for security reasons
allowRunningInsecureContent: true, // to serve from localhost
contextIsolation: true, // enabled for security reasons
enableRemoteModule: false, // disabled for security reasons
preload: path.resolve(__dirname, 'preload.js'),
},
});
if (serve) {
win.webContents.openDevTools();
require('electron-reload')(path.join(__dirname), {
electron: path.join(__dirname, '../../node_modules/.bin/electron'),
argv: ['--serve'],
});
win.loadURL('http://localhost:4200');
} else {
win.loadURL(
url.format({
pathname: path.join(__dirname, '../../src/dist/index.html'),
protocol: 'file:',
slashes: true,
}),
);
}
// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store window
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
});
return win;
}
function main(): void {
try {
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
// Added 400 ms to fix the black background issue while using transparent window.
// More detais at https://github.com/electron/electron/issues/15947
app.on('ready', () => {
setTimeout(createWindow, 400);
});
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});
} catch (e) {
// Catch Error
// throw e;
}
}
main();
electron/src/preload.ts
with:import { contextBridge, ipcRenderer } from 'electron';
const IN_EVENTS = [];
const OUT_EVENTS = [];
contextBridge.exposeInMainWorld('eventsApi', {
send: (channel, ...data): void => {
if (IN_EVENTS.includes(channel)) {
ipcRenderer.send(channel, ...data);
}
},
receive: (channel, cb): void => {
if (OUT_EVENTS.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => cb(...args));
}
},
invoke: async (channel, ...data): Promise<any> => {
if (IN_EVENTS.includes(channel)) {
return await ipcRenderer.invoke(channel, ...data);
}
},
});
electron/tsconfig.spec.json
for unit tests with:{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/electron/spec"
},
"include": ["**/*.spec.ts"]
}
electron/jest.config.js
with:const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { compilerOptions } = require('./tsconfig.json');
module.exports = {
verbose: true,
coverageDirectory: '../coverage/electron/ut',
collectCoverageFrom: ['src/**/*.ts', '!src/**/index.d.ts'],
transform: { '\\.ts$': ['ts-jest'] },
globals: {
'ts-jest': {
tsConfig: 'electron/tsconfig.spec.json',
},
},
reporters: [
'default',
[
'jest-junit',
{
suiteName: 'Unit tests',
outputDirectory: 'reports/electron',
outputName: 'ut.xml',
},
[
'../node_modules/jest-html-reporter',
{
pageTitle: 'Unit tests Report',
outputPath: 'reports/electron/ut/ut.html',
includeFailureMsg: true,
},
],
],
],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, {
prefix: '<rootDir>',
}),
};
*.spec.ts
files.$ yarn add -D spectron
Create folder e2e/src
.
Create file e2e/tsconfig.json
with:
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e"
},
"include": ["src/**/*.e2e.ts"]
}
e2e/jest.config.js
with:const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { compilerOptions } = require('./tsconfig.json');
module.exports = {
verbose: true,
testMatch: ['**/*.e2e.ts'],
transform: { '\\.ts$': ['ts-jest'] },
globals: {
'ts-jest': {
tsConfig: 'e2e/tsconfig.json',
},
},
testEnvironment: 'node',
reporters: [
'default',
[
'jest-junit',
{
suiteName: 'E2E tests',
outputDirectory: 'reports/e2e',
outputName: 'e2e.xml',
},
[
'../node_modules/jest-html-reporter',
{
pageTitle: 'E2E tests Report',
outputPath: 'reports/e2e/e2e.html',
includeFailureMsg: true,
},
],
],
],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, {
prefix: '<rootDir>/..',
}),
};
electron-builder
:$ yarn add -D electron-builder
electron-builder.json
with:{
"appId": "com.electron.angular-electron",
"productName": "angular-electron",
"directories": {
"output": "release/"
},
"files": ["electron/dist", "src/dist"],
"win": {
"icon": "src/dist/assets/icons/favicon.ico",
"target": ["portable"]
},
"mac": {
"icon": "src/dist/assets/icons/favicon.ico",
"target": ["dmg"]
},
"linux": {
"icon": "src/dist/assets/icons/favicon.256x256.png",
"target": ["AppImage"]
}
}
Init
yarn add -D npm-run-all make-dir make-dir-cli
Git hooks:
yarn add -D husky @commitlint/cli @commitlint/config-conventional commitizen cz-conventional-changelog lint-staged
UI design:
# Material (added with ng add @angular/material command)
$ yarn add @angular/material @angular/cdk hammerjs
# TailwindCSS
$ yarn add -D tailwindcss ng-tailwindcss
Testing:
$ yarn add -D jest jest-junit jest-preset-angular ts-jest @types/jest jest-html-reporter ngx-build-plus istanbul-instrumenter-loader @istanbuljs/nyc-config-typescript source-map-support ts-node @cypress/code-coverage nyc istanbul-lib-coverage wait-on @briebug/cypress-schematic
Documentation:
$ yarn add -D @compodoc/compodoc
Formatting:
$ yarn add -D prettier tslint-config-prettier tslint-plugin-prettier
Linting:
$ yarn add -D tslint-angular stylelint stylelint-config-recommended stylelint-junit-formatter stylelint-no-unsupported-browser-features stylelint-config-prettier stylelint-prettier
Electron
$ yarn add -D electron electron-builder spectron
$ yarn remove jasmine-core jasmine-spec-reporter karma karma-chrome-launcher karma-coverage-istanbul-reporter karma-jasmine karma-jasmine-html-reporter protractor @types/jasmine @types/jasminewd2