Get milliseconds of date in specific timezone

Moment.js is a great choice if you want to work with dates and times in JavaScript. It is very handy for creating interchange representations like ISO 8601 strings, taking your current timezone into account.

Convert to milliseconds

Let’s say you live in Berlin (Germany) and you want to represent June, 26th of 2019 at midnight in milliseconds with Central European Summer Time (GMT+2). With Moment.js v2.24 you have at least the following possibilities to do that:

Defined UTC Offset

defined-utc-offset.js
1
2
3
const moment = require('moment');
const unixTimestamp = moment('2019-06-26T00:00:00.000+02:00').valueOf();
console.log('unixTimestamp', unixTimestamp); // 1561500000000

Detected UTC Offset

detected-utc-offset.js
1
2
3
4
5
const moment = require('moment');
const utcOffsetInMinutes = new Date().getTimezoneOffset(); // -120 (2 hours)
const utcOffsetInMillis = utcOffsetInMinutes * 60000;
const unixTimestamp = moment('2019-06-26T00:00:00.000Z').valueOf() + utcOffsetInMillis;
console.log('unixTimestamp', unixTimestamp); // 1561500000000

Defined Zone Info

defined-zone-info.js
1
2
3
4
const moment = require('moment-timezone');
const zoneInfo = 'Europe/Berlin';
const unixTimestamp = moment.tz('2019-06-26 00:00:00', zoneInfo).valueOf();
console.log('unixTimestamp', unixTimestamp); // 1561500000000

Detected Zone Info

detected-zone-info.js
1
2
3
4
const moment = require('moment-timezone');
const zoneInfo = moment.tz.guess();
const unixTimestamp = moment.tz('2019-06-26 00:00:00', zoneInfo).valueOf();
console.log('unixTimestamp', unixTimestamp); // 1561500000000

Pro Tip: You can get date and time expressed according to ISO 8601 in JavaScript from the current date when calling:

1
new Date().toISOString(); // 019-07-03T13:12:18.784Z

Note that the above string will always be normalized to UTC time (Z), so you need to apply the UTC offset yourself.

TypeScript code coverage with Karma

Baseline

  • Node.js v10.9.0
  • yarn v1.15.2
package.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"devDependencies": {
"jasmine": "3.4.0"
},
"main": "src/main.js",
"name": "karma-webpack-babel-typescript-istanbul",
"scripts": {
"start": "node index.js",
"test": "jasmine --config=jasmine.json"
},
"version": "0.0.0"
}
jasmine.json
1
2
3
4
5
6
7
8
{
"random": true,
"spec_dir": "src",
"spec_files": [
"**/*test.js"
],
"stopSpecOnExpectationFailure": true
}
index.js
1
2
3
4
5
const afterTwoSeconds = require('./src/main');

afterTwoSeconds(() => {
console.log('I will be called after 2 seconds.');
});
src/main.js
1
2
3
4
5
6
7
8
module.exports = function afterTwoSeconds(callback) {
return new Promise((resolve) => {
setTimeout(() => {
callback();
resolve();
}, 2000);
});
};
src/main.test.js
1
2
3
4
5
6
7
8
9
const afterTwoSeconds = require('./main');

describe('afterTwoSeconds', () => {
it('resolves after 2 seconds', async () => {
const myCallbackSpy = jasmine.createSpy('myCallbackSpy');
await afterTwoSeconds(myCallbackSpy);
expect(myCallbackSpy).toHaveBeenCalled();
});
});

Add Karma

Update dependencies

Karma needs an adapter to know about the Jasmine testing framework. It also needs a browser launcher to run the tests within a browser environment.

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"devDependencies": {
"jasmine": "3.4.0",
"karma": "4.1.0",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "2.0.1"
},
"main": "src/main.js",
"name": "karma-webpack-babel-typescript-istanbul",
"scripts": {
"start": "node index.js",
"test": "karma start"
},
"version": "0.0.0"
}

Update export

Karma will run the tests inside the operating system’s Chrome browser which was in my case Chrome v74. The browser environment does not know about module.exports, so we will use the window namespace to export our afterTwoSeconds function:

src/main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function afterTwoSeconds(callback) {
return new Promise((resolve) => {
setTimeout(() => {
callback();
resolve();
}, 2000);
});
}

if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = afterTwoSeconds;
} else {
window.afterTwoSeconds = afterTwoSeconds;
}

Note: We still export our code for Node.js environments, to make our code work in both worlds. That’s why we keep setTimeout because it is available in Node.js and browser environments. If we would write window.setTimeout it would only work in browsers but fail in Node.js.

Update test

Our tests will run in the browser so we cannot import code with a require statement (CommonJS syntax) anymore and need to use the window namespace:

src/main.test.js
1
2
3
4
5
6
7
describe('afterTwoSeconds', () => {
it('resolves after 2 seconds', async () => {
const myCallbackSpy = jasmine.createSpy('myCallbackSpy');
await window.afterTwoSeconds(myCallbackSpy);
expect(myCallbackSpy).toHaveBeenCalled();
});
});

Add Karma configuration

A Karma configuration can be interactively created by running npx karma init. In our case we reuse the file paths from our Jasmine configuration.

For a successful Karma test run it is important to declare the source code and test code within the files property.

Karma can be equipped with a custom test reporter but for now we are good with the standard progress reporter:

karma.conf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const jasmineConfig = require('./jasmine.json');

module.exports = function (config) {
config.set({
autoWatch: false,
basePath: jasmineConfig.spec_dir,
browsers: ['Chrome'],
colors: true,
concurrency: Infinity,
exclude: [],
files: [
'main.js',
...jasmineConfig.spec_files
],
frameworks: ['jasmine'],
logLevel: config.LOG_INFO,
port: 9876,
preprocessors: {},
reporters: ['progress'],
singleRun: true,
});
};

Add Webpack

Update dependencies

We need to add webpack-karma so that Karma can use webpack to preprocess files. This also requires us to include webpack in our list of dependencies as it is a peer dependency of webpack-karma:

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"devDependencies": {
"jasmine": "3.4.0",
"karma": "4.1.0",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "2.0.1",
"karma-webpack": "3.0.5",
"webpack": "4.30.0"
},
"main": "src/main.js",
"name": "karma-webpack-babel-typescript-istanbul",
"scripts": {
"start": "node index.js",
"test": "karma start"
},
"version": "0.0.0"
}

Common mistake

ERROR [preprocess]: Can not load “webpack”, it is not registered!
Perhaps you are missing some plugin?

This happens when you run karma start and you forgot to install webpack.

Add Webpack configuration

Thanks to webpack’s zero configuration mode and its default settings, we don’t need to specify much. All we do is defining a “development” mode to get detailed messages in case of preprocessing errors:

webpack.config.js
1
2
3
module.exports = {
mode: 'development'
};

Using a “development” mode will decrypt error messages like TypeError: r is not a function.

Update Karma configuration

In the previous Karma setup, our test code was relying that our business logic is exposed to the window namespace (window.afterTwoSeconds). Having webpack in place we will now load our business logic through our test code. That’s why we don’t need to declare our business logic anymore within Karma’s files pattern. It’s sufficient if we just point Karma to our test code because the tests will import the main source code for us. We can also reuse our webpack configuration by requiring it. To activate webpack, we need to declare it as a preprocessor for our test code. We also need to add the webpack configuration to our Karma configuration:

karma.conf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const testCode = 'src/**/*test.js';
const webpackConfig = require('./webpack.config.js');

module.exports = function (config) {
config.set({
autoWatch: false,
basePath: '',
browsers: ['Chrome'],
colors: true,
concurrency: Infinity,
exclude: [],
files: [
{pattern: testCode, watched: false}
],
frameworks: ['jasmine'],
logLevel: config.LOG_INFO,
port: 9876,
preprocessors: {
[testCode]: ['webpack']
},
reporters: ['progress'],
singleRun: true,
webpack: webpackConfig
});
};

Update imports

Our test code will now be preprocessed by webpack which means that we can use Node.js features like require statements to import code. Webpack will make sure that the require statements get processed into something that can be understood by our Browser environment:

karma.conf.js
1
2
3
4
5
6
7
8
9
const afterTwoSeconds = require('./main');

describe('afterTwoSeconds', () => {
it('resolves after 2 seconds', async () => {
const myCallbackSpy = jasmine.createSpy('myCallbackSpy');
await afterTwoSeconds(myCallbackSpy);
expect(myCallbackSpy).toHaveBeenCalled();
});
});

Add Babel

Babel 7 ships with TypeScript support and can be used to preprocess code with TypeScript’s compiler. You might not need Babel to compile your code with TypeScript but using Babel’s ecosystem (with presets like @babel/preset-env) can bring enormous benefits if you want to ship code for various environments. That’s why it is the preferred setup in this tutorial, so let’s get started with a Babel setup:

Update dependencies

1
yarn add --dev @babel/core babel-loader
package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"devDependencies": {
"@babel/core": "7.4.4",
"babel-loader": "8.0.5",
"jasmine": "3.4.0",
"karma": "4.1.0",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "2.0.1",
"karma-webpack": "3.0.5",
"webpack": "4.30.0"
},
"main": "src/main.js",
"name": "karma-webpack-babel-typescript-istanbul",
"scripts": {
"start": "node index.js",
"test": "karma start"
},
"version": "0.0.0"
}

Add Babel configuration

We will start with a very basic Babel configuration which does not define any plugin our sets of plugins (called presets). Without plugins Babel won’t do much which is okay for now and will be changed once we add TypeScript to our Babel toolchain.

babel.config.js
1
2
3
4
module.exports = {
plugins: [],
presets: [],
};

Update webpack configuration

With the babel-loader we are telling webpack to process files ending on .js or .jsx (/\.jsx?$/) through Babel:

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
mode: 'development',
module: {
rules: [
{
exclude: /(node_modules)/,
loader: 'babel-loader',
test: /\.jsx?$/,
},
],
},
};

Add TypeScript

Update dependencies

1
yarn add @babel/preset-typescript @types/jasmine @types/node typescript --dev
package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"devDependencies": {
"@babel/core": "7.4.4",
"@babel/preset-typescript": "7.3.3",
"@types/jasmine": "3.3.12",
"@types/node": "12.0.0",
"babel-loader": "8.0.5",
"jasmine": "3.4.0",
"karma": "4.1.0",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "2.0.1",
"karma-webpack": "3.0.5",
"typescript": "3.4.5",
"webpack": "4.30.0"
},
"main": "src/main.js",
"name": "karma-webpack-babel-typescript-istanbul",
"scripts": {
"start": "node index.js",
"test": "karma start"
},
"version": "0.0.0"
}

Add TypeScript configuration

1
tsc --init
tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
{
"compilerOptions": {
"esModuleInterop": true,
"lib": ["es6"],
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"target": "es6"
}
}

Update webpack configuration

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
mode: 'development',
module: {
rules: [
{
exclude: /(node_modules)/,
loader: 'babel-loader',
test: /\.[tj]sx?$/,
},
],
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
};

Update Babel configuration

babel.config.js
1
2
3
4
module.exports = {
plugins: [],
presets: ['@babel/preset-typescript'],
};

Migrate test code

We need to rename main.test.js to main.test.ts. Thanks to the allowJs TypeScript compiler option we can still import our JavaScript business logic within our test code:

src/main.test.ts
1
2
3
4
5
6
7
8
9
const afterTwoSeconds = require('./main');

describe('afterTwoSeconds', () => {
it('resolves after 2 seconds', async () => {
const myCallbackSpy = jasmine.createSpy('myCallbackSpy');
await afterTwoSeconds(myCallbackSpy);
expect(myCallbackSpy).toHaveBeenCalled();
});
});

Update Karma configuration

Our Karma setup now needs to load our migrated test code:

karma.conf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const testCode = 'src/**/*test.ts';
const webpackConfig = require('./webpack.config.js');

module.exports = function (config) {
config.set({
autoWatch: false,
basePath: '',
browsers: ['Chrome'],
colors: true,
concurrency: Infinity,
exclude: [],
files: [
{pattern: testCode, watched: false}
],
frameworks: ['jasmine'],
logLevel: config.LOG_INFO,
port: 9876,
preprocessors: {
[testCode]: ['webpack']
},
reporters: ['progress'],
singleRun: true,
webpack: webpackConfig
});
};

Migrate source code

Update export

src/main.ts
1
2
3
4
5
6
7
8
export function afterTwoSeconds(callback: Function) {
return new Promise((resolve) => {
setTimeout(() => {
callback();
resolve();
}, 2000);
});
}

Update import

src/main.test.ts
1
2
3
4
5
6
7
8
9
import {afterTwoSeconds} from './main';

describe('afterTwoSeconds', () => {
it('resolves after 2 seconds', async () => {
const myCallbackSpy = jasmine.createSpy('myCallbackSpy');
await afterTwoSeconds(myCallbackSpy);
expect(myCallbackSpy).toHaveBeenCalled();
});
});

Adjust start script

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"devDependencies": {
"@babel/core": "7.4.4",
"@babel/preset-typescript": "7.3.3",
"@types/jasmine": "3.3.12",
"@types/node": "12.0.0",
"babel-loader": "8.0.5",
"jasmine": "3.4.0",
"karma": "4.1.0",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "2.0.1",
"karma-webpack": "3.0.5",
"typescript": "3.4.5",
"webpack": "4.30.0"
},
"main": "dist/main.js",
"name": "karma-webpack-babel-typescript-istanbul",
"scripts": {
"start": "tsc && node dist/main.js",
"test": "karma start"
},
"version": "0.0.0"
}

Add code coverage

Note: Every package prefixed with karma- will be automatically added to Karma’s plugin section, so no need to define it.

Update dependencies

1
yarn add istanbul-instrumenter-loader karma-coverage-istanbul-reporter --dev

Update Karma configuration

1
yarn add istanbul-instrumenter-loader karma-coverage-istanbul-reporter --dev
package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"devDependencies": {
"@babel/core": "7.4.4",
"@babel/preset-typescript": "7.3.3",
"@types/jasmine": "3.3.12",
"@types/node": "12.0.0",
"babel-loader": "8.0.5",
"istanbul-instrumenter-loader": "3.0.1",
"jasmine": "3.4.0",
"karma": "4.1.0",
"karma-chrome-launcher": "2.2.0",
"karma-coverage-istanbul-reporter": "2.0.5",
"karma-jasmine": "2.0.1",
"karma-webpack": "3.0.5",
"typescript": "3.4.5",
"webpack": "4.30.0"
},
"main": "dist/main.js",
"name": "karma-webpack-babel-typescript-istanbul",
"scripts": {
"start": "tsc && node dist/main.js",
"test": "karma start"
},
"version": "0.0.0"
}

Update webpack configuration

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
mode: 'development',
module: {
rules: [
{
exclude: /(node_modules)/,
loader: 'babel-loader',
test: /\.[tj]sx?$/,
},
{
enforce: 'post',
exclude: /(node_modules|\.test\.[tj]sx?$)/,
test: /\.[tj]s$/,
use: {
loader: 'istanbul-instrumenter-loader',
options: {esModules: true}
},
}
],
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
};

Update Karma configuration

karma.conf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const testCode = 'src/**/*test.ts';
const webpackConfig = require('./webpack.config.js');

module.exports = function (config) {
config.set({
autoWatch: false,
basePath: '',
browsers: ['Chrome'],
colors: true,
concurrency: Infinity,
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
reports: ['html']
},
exclude: [],
files: [
{pattern: testCode, watched: false}
],
frameworks: ['jasmine'],
logLevel: config.LOG_INFO,
port: 9876,
preprocessors: {
[testCode]: ['webpack']
},
reporters: ['progress', 'coverage-istanbul'],
singleRun: true,
webpack: webpackConfig
});
};

Bonus: TypeScript everything!

You might not need aliases

Importing a function in ES6 can look like this:

1
import {getIndexValue} from './math/FibonacciSequence';
  • problems with tsc
  • you need a config (think of webpack zero config trend)
  • it is not clear if node modules or not (think of lodash/…)
  • adds a layer of complexity
  • IDE loss of auto import (needs to be adjusted)
  • IDE loss of jump into that file
  • IDE can change paths anyway
  • it adds convenience which is given by the IDE already
  • Turn “util” often becomes “utils” because of clash with node core library

Anatomy of an Electron 4 application

Overview of main concepts in Electron 4 applications.

main process

Concerns

  • Entry point for Electron applications
  • Creates and manages BrowserWindow instances
  • Registers global shortcuts
  • Creates native menus
  • Shows native GUI
  • Responds to auto-update events

renderer process

Concerns

  • Takes care of showing your HTML & JS in the Chromium browser
  • Runs UI in webContents instances
  • Access information about audio and video devices using desktopCapturer
  • Can access main process modules via remote module

Characteristics

  • process.type is "renderer"

Example

1
2
3
4
5
6
7
const {desktopCapturer, ipcRenderer, webFrame} = require('electron');
const {app} = require('electron').remote

webFrame.setZoomFactor(1.0);
webFrame.setVisualZoomLevelLimits(1, 1);

console.log('App configuration directory', app.getPath('userData'));

Inter-process communication (IPC)

Using inter-process communication a renderer process can exchange messages with a main process:

renderer.js

1
2
3
4
5
6
7
import {ipcRenderer} from 'electron';

const updateBtn = document.getElementById('updateBtn')

updateBtn.addEventListener('click', () => {
ipcRenderer.send('my-app-event', document.getElementById('notifyVal').value);
});

main.ts

1
2
3
4
5
6
7
import {BrowserWindow, ipcMain, IpcMessageEvent} from 'electron';

const main = new BrowserWindow();

ipcMain.on('my-app-event', (event: IpcMessageEvent, price: number) => {
main.webContents.send('target-price', price);
});

Note: ipcRenderer does not send messages to itself, it sends them to ipcMain. If you want to access the messages within a renderer process, you need to check ipcMain using electron.remote:

renderer.js

1
2
3
4
5
6
7
import {remote} from 'electron';

// ...

remote.ipcMain.on('my-app-event', (event, price) => {
console.log(`Received "${price}" in renderer process.`);
});

Webview Preload Script

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<script>
require('./renderer.js');
</script>
<body>
<webview
preload="./preload.js"
src="https://benny.work"
></webview>
</body>
</html>

You can assign a preload script programmatically:

1
2
3
4
5
6
7
8
9
import fileUrl = require('file-url');

const main = new BrowserWindow();

const contents = main.webContents;

contents.on('will-attach-webview', (event, webPreferences, params) => {
webPreferences.preloadURL = fileUrl('./preload.js');
});

preload.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const {ipcRenderer} = require('electron');

window.addEventListener('DOMContentLoaded', () => {
// From guest (webview content) to host (main process)
window.addEventListener(z.event.WebApp.LIFECYCLE.RESTART, (event) => {
ipcRenderer.send(EVENT_TYPE.WRAPPER.RELAUNCH);
});

// From host (main process) to guest (webview content)
ipcRenderer.on(EVENT_TYPE.WRAPPER.RELAUNCHED, () => {
window.dispatchEvent(new CustomEvent(EVENT_TYPE.ACTION.CREATE_ACCOUNT));
});
});

main.ts

1
2
3
4
5
6
7
8
9
10
11
import {BrowserWindow} from 'electron';

const main = new BrowserWindow();
main.loadFile('index.html');
ipcMain.on(
EVENT_TYPE.WRAPPER.RELAUNCH,
async (event: IpcMessageEvent) => {
console.log('Do some work and send a reply...');
main.webContents.send(EVENT_TYPE.WRAPPER.RELAUNCHED);
}
);

Setup TypeScript code coverage for Electron applications

Environment

  • Node.js v10.9.0

To receive code coverage based on TypeScript source code within an Electron environment, we need to do the following:

  1. Tell electron-mocha to use babel
  2. Register @babel/preset-typescript in babel to compile our code on-the-fly
  3. Register istanbul plugin in babel to instrument our compiled code
  4. Register a after hook in mocha to write out our coverage information (provided by istanbul)
  5. Run nyc to create a HTML report from our coverage information stored in .nyc_output

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"devDependencies": {
"@babel/core": "7.3.3",
"@babel/plugin-proposal-class-properties": "7.3.3",
"@babel/preset-env": "7.3.1",
"@babel/preset-typescript": "7.3.3",
"@babel/register": "7.0.0",
"@types/mocha": "5.2.6",
"babel-plugin-istanbul": "5.1.1",
"electron": "4.0.5",
"electron-mocha": "6.0.4",
"nyc": "13.3.0",
"typescript": "3.3.3"
},
"main": "dist/main.js",
"scripts": {
"coverage": "yarn test && nyc report",
"test": "electron-mocha --require ./babel-register.js src/**/*.test.main.ts"
},
"version": "0.0.0"
}

babel-register.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require('@babel/register')({
cache: false,
extensions: ['.ts'],
plugins: [
'@babel/proposal-class-properties',
[
'istanbul',
{
exclude: ['**/*.test*.ts'],
},
],
],
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
'@babel/preset-typescript',
],
});
tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist",
"rootDir": "src",
"target": "es5"
},
"exclude": [
"dist",
"node_modules"
]
}

.nycrc.json

1
2
3
4
5
6
7
8
{
"all": true,
"exclude": ["**/*.test*.ts"],
"extension": [".ts"],
"include": ["src/**/*.ts"],
"per-file": false,
"reporter": ["html"]
}

after.test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import * as fs from 'fs-extra';
import * as path from 'path';

declare global {
namespace NodeJS {
interface Global {
__coverage__: {};
}
}
}

const writeCoverageReport = (coverage: Object) => {
const outputFile = path.resolve(process.cwd(), `.nyc_output/coverage.${process['type']}.json`);
fs.outputJsonSync(outputFile, coverage);
};

after(() => {
const coverageInfo = global.__coverage__;
if (coverageInfo) {
writeCoverageReport(coverageInfo);
}
});

Takeaways

  • nyc can combine multiple coverage files (like coverage.json) into one report
  • eletron-mocha can run tests in an Electron renderer process and in an Electron main process
  • To get full code coverage, there needs to be a test run with test files for the main process (*.test.main.ts) and a second run for tests from the renderer process (*.test.renderer.ts)
  • Electron provides a process.type (can be browser or renderer) to indicate in which process the code runs
  • To have a universal after hook for both test runs, the process.type can be used in the coverage output file name
  • istanbul needs to exclude the test files, otherwise it will report code coverage for the test files too

Run Node.js apps on Heroku

Getting Started on Heroku

The best way to get started on the Heroku polyglot platform is to follow their fantastic introduction. It’s also worth reading about their supported environments, deployment tasks and European deployment region.

To speed things a little bit up, I am providing a list of resources which I found useful when deploying my first Node.js web applications on Heroku.

Bootstrap Node.js environment

  1. Download the Heroku CLI (or Heroku Toolbelt)
  2. Run heroku --version to see if it works (I tested with v6.15.5)
  3. Run heroku login
  4. Run heroku whoami to see if you are logged in
  5. Run heroku create --region eu --buildpack heroku/nodejs to create a Node.js app on Heroku in a European data center (your app will get a URL like https://app-name-number.herokuapp.com/)
  6. Run heroku open -a app-name-number to see your application in a browser

Connect Git repository

By default web applications created on Heroku (with heroku create) come with their own Git repository. If you are starting completely from scratch, then you can follow these steps to push your own code to Heroku’s Git repository:

1
2
3
4
5
6
git init
git remote add origin https://git.heroku.com/app-name-number.git
npm init -y
git add .
git commit -m "Initial commit"
git push -u origin master

If you now execute heroku open, you will see an “Application error” because you need to specify a way to start a Node.js process (most likely through npm start).

Note: Heroku will also run npm run build by default before running npm start.

Write Node.js application

When npm init -y was executed, a package.json file has been created. We will make some adjustments to that file and create a web application based on the Express web framework:

  1. Run npm i --save-dev typescript
  2. Run npm i --save express @types/express
  3. Run npx tsc --init
  4. Modify source code to match the following files.

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"dependencies": {
"@types/express": "4.16.1",
"express": "4.16.4"
},
"devDependencies": {
"typescript": "3.3.3333"
},
"engines": {
"node": "11.x.x"
},
"license": "ISC",
"main": "dist/main.js",
"name": "my-app",
"repository": {
"type": "git",
"url": "https://git.heroku.com/app-name-number.git"
},
"scripts": {
"build": "tsc --build tsconfig.json",
"start": "node dist/main.js"
},
"version": "1.0.0"
}

src/main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import express from 'express';

const pkg = require('../package.json');
const app = express();

app.set('port', process.env.PORT || 3000);

app.all('*', (request, response) => {
response.send(`<b>${pkg.name} v${pkg.version}</b>`);
});

app.listen(app.get('port'), () => {
console.log(`Server is running on port "${app.get('port')}".`);
});

Deploy latest code changes:

1
2
3
git add .
git commit -m "Show app version"
git push

Connect GitHub repository

To run deployments from your GitHub repository, you need to add Heroku’s Git repository to your cloned GitHub repository:

1
2
3
4
5
# Add remote named "heroku"
git remote add heroku https://git.heroku.com/app-name-number.git

# Push to "master" branch on remote "heroku"
git push heroku master

You can also connect your Heroku application with code from GitHub by using Heroku’s GitHub Deployments from the app dashboard.

Get logs

1
heroku logs --tail -a app-name-number

Run CLI apps

If you want to run a pure command-line app which does not serve a webpage, then you can change your use a “worker” dyno instead of a “web” done. Just place a file called Procfile in the root of your project and define the start script for the “worker” dyno:

Procfile

1
worker: npm start

Tip: Prefer “npm start” over “node dist/main.js” to run in the npm context and to have access to environment variables like process.env.npm_package_name.

Next thing you should do is to scale down your “web” dyno, if you just want to run one “worker” dyno:

1
heroku ps:scale web=0 worker=1 -a app-name-number

Heroku runs health checks on the web domain of your application. That’s why you need to scale down the “web” dyno if you just use a CLI app because otherweise the web health check will fail (see example below) and Heroku will kill your application:

1
2
3
4
5
6
2019-03-19T22:57:01.212233+00:00 heroku[router]: at=error code=H20 desc="App boot timeout" method=GET path="/" host=app-name-number.herokuapp.com request_id=7832
926b-a547-4927-9895-ea8a12e42765 fwd="91.10.153.93" dyno= connect= service= status=503 bytes= protocol=https
2019-03-19T22:57:49.960888+00:00 heroku[web.1]: State changed from starting to crashed
2019-03-19T22:57:49.863438+00:00 heroku[web.1]: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch
2019-03-19T22:57:49.863489+00:00 heroku[web.1]: Stopping process with SIGKILL
2019-03-19T22:57:49.943431+00:00 heroku[web.1]: Process exited with status 137

Databases

Databases on Heroku are handled as “Add-ons”. You can get a “Heroku Postgress“ database for free. Connecting your Node.js application with it is super simple. Just add the database from your Heroku application dashboard as “Add-on” and Heroku will handle the rest for you and provide a process.env.DATABASE_URL variable that can be used to connect via object-relational mappers like TypeORM.

Note: You can also get the current connection properties (database name, user, password, port, etc.) from the settings panel of your data store on data.heroku.com but be aware that Heroku rotates credentials periodically so it’s advisable to rely on the database connection url instead of a username and password combination.

initDatabase.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import 'reflect-metadata';
import {Connection, createConnection} from 'typeorm';
import {SqliteConnectionOptions} from 'typeorm/driver/sqlite/SqliteConnectionOptions';
import {PostgresConnectionOptions} from 'typeorm/driver/postgres/PostgresConnectionOptions';

export default function initDatabase(): Promise<Connection> {
const localhost: SqliteConnectionOptions = {
database: 'test.db3',
type: 'sqlite'
};

const production: PostgresConnectionOptions = {
type: 'postgres',
url: process.env.DATABASE_URL
};

const connectionOptions = (process.env.NODE_ENV === 'production') ? production : localhost;

Object.assign(connectionOptions, {
entities: [
'src/entity/**/*.ts'
],
logging: false,
migrations: [
'src/migration/**/*.ts'
],
subscribers: [
'src/subscriber/**/*.ts'
],
synchronize: true,
});

return createConnection(connectionOptions);
};

You will also need to have these dependencies in your package.json file:

1
2
3
4
"pg": "7.9.0",
"typeorm": "0.2.15",
"reflect-metadata": "0.1.10",
"sqlite3": "4.0.3"

Backups

  • You can find your databases on data.heroku.com
  • Use the following command to turn binary database backups into plain text: pg_restore backup.bin > backup.sql

If you look for a free tool to connection to your Heroku Postgres database, then have a look at pgAdmin.

Parse command line arguments in Node.js

There are many libraries to build CLI tools for Node.js like optimist, minimist, yargs, Caporal.js and commander to name just a few.

My favorite one is commander because it comes with TypeScript definitions:

commander-example.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env node

import program from 'commander';

const pkg = require('../package.json');
const appName = Object.keys(pkg.bin)[0];

program
.description(pkg.description)
.name(appName)
.option('-c, --config <path>', 'set path to configuration file')
.version(pkg.version)
.parse(process.argv);

console.log('Configuration path', program.config);

Biggest lack on commander.js is that it does not fail if you have mandatory arguments but don’t supply any argument at all. For example it fails when calling node program.js -c because there is no value for c but it doesn’t fail when just calling node program.js. Tested with commander v2.19.0. GitHub issue.

Luckily, Caporal.js addresses this issue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env node

import program from 'caporal';

const pkg = require('../package.json');
const appName = Object.keys(pkg.bin)[0];

program
.version(pkg.version)
.description(pkg.description)
.name(appName)
.option('--config <config>', 'set path to configuration file', undefined, undefined, true)
.action((args, options, logger) => {
logger.info(`Configuration value: ${options.config}`);
});

program.parse(process.argv);

Error: Missing option –config

Should you just care about getting arguments in an easy way, you will be good with minimist:

1
2
3
4
import * as minimist from 'minimist';

const argv = minimist(process.argv.slice(1));
console.log(argv.mySpecialFlag);

Extras

If you want to equip your program with some kind of configuration file (à la webpack.config.js or .babelrc) I can recommend cosmiconfig to you. Cosmiconfig searches for and loads configuration files with the extensions .json, .yaml, .yml, or .js.

And if you need pattern matching you will probably fall in love with Globs which are implemened by minimatch.

If you need an interactive command line user interface with prompts, try Inquirer.js.

And for more colorful log messages on the terminal, there is chalk.

Add a window property with TypeScript

Define a window property

In my case TypeScript did not know about window.__coverage__ which is a property that is set by istanbul’s instrumenter class.

When trying to use if TypeScript will complain:

TS2339: Property ‘coverage‘ does not exist on type ‘Window’.

To get along with it we need to make TypeScript aware of this new property by declaring it in our TypeScript code:

1
2
3
4
5
declare global {
interface Window {
__coverage__: Object;
}
}

Define a global property

If you want to define a global __coverage__ property in a Node.js environment, you will have to assign it to the global object:

1
2
3
4
5
6
7
declare global {
namespace NodeJS {
interface Global {
__coverage__: {};
}
}
}

Setup electron-mocha with @babel/register and TypeScript

Environment

  • Node.js v10.9.0

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"devDependencies": {
"@babel/core": "7.3.3",
"@babel/plugin-proposal-class-properties": "7.3.3",
"@babel/preset-env": "7.3.1",
"@babel/preset-typescript": "7.3.3",
"@babel/register": "7.0.0",
"@types/mocha": "5.2.6",
"electron": "4.0.5",
"electron-mocha": "6.0.4",
"typescript": "3.3.3"
},
"main": "dist/main.js",
"scripts": {
"test": "electron-mocha --require ./babel-register.js src/**/*.test.ts"
},
"version": "0.0.0"
}

babel-register.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require('@babel/register')({
cache: false,
extensions: ['.ts'],
plugins: ['@babel/proposal-class-properties'],
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
'@babel/preset-typescript',
],
});

tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist",
"rootDir": "src",
"target": "es5"
},
"exclude": [
"dist",
"node_modules"
]
}

Takeaways

  • --require ./babel-register.js must be used because you need to specify extensions for TypeScript and this cannot be done when using just --require @babel/register
  • @babel/preset-env is required with a node target because electron-mocha runs in a Node.js environment
  • @babel/preset-typescript is used to turn tests written in TypeScript into JavaScript: https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/
  • The @babel/proposal-class-properties is optional and only required if you code makes already use of class properties

Good to know