Index: frontend/node_modules/istanbul-lib-instrument/src/constants.js
===================================================================
--- frontend/node_modules/istanbul-lib-instrument/src/constants.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/istanbul-lib-instrument/src/constants.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,14 @@
+const { createHash } = require('crypto');
+const { name } = require('../package.json');
+// TODO: increment this version if there are schema changes
+// that are not backwards compatible:
+const VERSION = '4';
+
+const SHA = 'sha1';
+module.exports = {
+    SHA,
+    MAGIC_KEY: '_coverageSchema',
+    MAGIC_VALUE: createHash(SHA)
+        .update(name + '@' + VERSION)
+        .digest('hex')
+};
Index: frontend/node_modules/istanbul-lib-instrument/src/index.js
===================================================================
--- frontend/node_modules/istanbul-lib-instrument/src/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/istanbul-lib-instrument/src/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,21 @@
+const { defaults } = require('@istanbuljs/schema');
+const Instrumenter = require('./instrumenter');
+const programVisitor = require('./visitor');
+const readInitialCoverage = require('./read-coverage');
+
+/**
+ * createInstrumenter creates a new instrumenter with the
+ * supplied options.
+ * @param {Object} opts - instrumenter options. See the documentation
+ * for the Instrumenter class.
+ */
+function createInstrumenter(opts) {
+    return new Instrumenter(opts);
+}
+
+module.exports = {
+    createInstrumenter,
+    programVisitor,
+    readInitialCoverage,
+    defaultOpts: defaults.instrumenter
+};
Index: frontend/node_modules/istanbul-lib-instrument/src/instrumenter.js
===================================================================
--- frontend/node_modules/istanbul-lib-instrument/src/instrumenter.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/istanbul-lib-instrument/src/instrumenter.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,162 @@
+/*
+ Copyright 2012-2015, Yahoo Inc.
+ Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+const { transformSync } = require('@babel/core');
+const { defaults } = require('@istanbuljs/schema');
+const programVisitor = require('./visitor');
+const readInitialCoverage = require('./read-coverage');
+
+/**
+ * Instrumenter is the public API for the instrument library.
+ * It is typically used for ES5 code. For ES6 code that you
+ * are already running under `babel` use the coverage plugin
+ * instead.
+ * @param {Object} opts optional.
+ * @param {string} [opts.coverageVariable=__coverage__] name of global coverage variable.
+ * @param {boolean} [opts.reportLogic=false] report boolean value of logical expressions.
+ * @param {boolean} [opts.preserveComments=false] preserve comments in output.
+ * @param {boolean} [opts.compact=true] generate compact code.
+ * @param {boolean} [opts.esModules=false] set to true to instrument ES6 modules.
+ * @param {boolean} [opts.autoWrap=false] set to true to allow `return` statements outside of functions.
+ * @param {boolean} [opts.produceSourceMap=false] set to true to produce a source map for the instrumented code.
+ * @param {Array} [opts.ignoreClassMethods=[]] set to array of class method names to ignore for coverage.
+ * @param {Function} [opts.sourceMapUrlCallback=null] a callback function that is called when a source map URL
+ *     is found in the original code. This function is called with the source file name and the source map URL.
+ * @param {boolean} [opts.debug=false] - turn debugging on.
+ * @param {array} [opts.parserPlugins] - set babel parser plugins, see @istanbuljs/schema for defaults.
+ * @param {string} [opts.coverageGlobalScope=this] the global coverage variable scope.
+ * @param {boolean} [opts.coverageGlobalScopeFunc=true] use an evaluated function to find coverageGlobalScope.
+ */
+class Instrumenter {
+    constructor(opts = {}) {
+        this.opts = {
+            ...defaults.instrumenter,
+            ...opts
+        };
+        this.fileCoverage = null;
+        this.sourceMap = null;
+    }
+    /**
+     * instrument the supplied code and track coverage against the supplied
+     * filename. It throws if invalid code is passed to it. ES5 and ES6 syntax
+     * is supported. To instrument ES6 modules, make sure that you set the
+     * `esModules` property to `true` when creating the instrumenter.
+     *
+     * @param {string} code - the code to instrument
+     * @param {string} filename - the filename against which to track coverage.
+     * @param {object} [inputSourceMap] - the source map that maps the not instrumented code back to it's original form.
+     * Is assigned to the coverage object and therefore, is available in the json output and can be used to remap the
+     * coverage to the untranspiled source.
+     * @returns {string} the instrumented code.
+     */
+    instrumentSync(code, filename, inputSourceMap) {
+        if (typeof code !== 'string') {
+            throw new Error('Code must be a string');
+        }
+        filename = filename || String(new Date().getTime()) + '.js';
+        const { opts } = this;
+        let output = {};
+        const babelOpts = {
+            configFile: false,
+            babelrc: false,
+            ast: true,
+            filename: filename || String(new Date().getTime()) + '.js',
+            inputSourceMap,
+            sourceMaps: opts.produceSourceMap,
+            compact: opts.compact,
+            comments: opts.preserveComments,
+            parserOpts: {
+                allowReturnOutsideFunction: opts.autoWrap,
+                sourceType: opts.esModules ? 'module' : 'script',
+                plugins: opts.parserPlugins
+            },
+            plugins: [
+                [
+                    ({ types }) => {
+                        const ee = programVisitor(types, filename, {
+                            coverageVariable: opts.coverageVariable,
+                            reportLogic: opts.reportLogic,
+                            coverageGlobalScope: opts.coverageGlobalScope,
+                            coverageGlobalScopeFunc:
+                                opts.coverageGlobalScopeFunc,
+                            ignoreClassMethods: opts.ignoreClassMethods,
+                            inputSourceMap
+                        });
+
+                        return {
+                            visitor: {
+                                Program: {
+                                    enter: ee.enter,
+                                    exit(path) {
+                                        output = ee.exit(path);
+                                    }
+                                }
+                            }
+                        };
+                    }
+                ]
+            ]
+        };
+
+        const codeMap = transformSync(code, babelOpts);
+
+        if (!output || !output.fileCoverage) {
+            const initialCoverage =
+                readInitialCoverage(codeMap.ast) ||
+                /* istanbul ignore next: paranoid check */ {};
+            this.fileCoverage = initialCoverage.coverageData;
+            this.sourceMap = inputSourceMap;
+            return code;
+        }
+
+        this.fileCoverage = output.fileCoverage;
+        this.sourceMap = codeMap.map;
+        const cb = this.opts.sourceMapUrlCallback;
+        if (cb && output.sourceMappingURL) {
+            cb(filename, output.sourceMappingURL);
+        }
+
+        return codeMap.code;
+    }
+    /**
+     * callback-style instrument method that calls back with an error
+     * as opposed to throwing one. Note that in the current implementation,
+     * the callback will be called in the same process tick and is not asynchronous.
+     *
+     * @param {string} code - the code to instrument
+     * @param {string} filename - the filename against which to track coverage.
+     * @param {Function} callback - the callback
+     * @param {Object} inputSourceMap - the source map that maps the not instrumented code back to it's original form.
+     * Is assigned to the coverage object and therefore, is available in the json output and can be used to remap the
+     * coverage to the untranspiled source.
+     */
+    instrument(code, filename, callback, inputSourceMap) {
+        if (!callback && typeof filename === 'function') {
+            callback = filename;
+            filename = null;
+        }
+        try {
+            const out = this.instrumentSync(code, filename, inputSourceMap);
+            callback(null, out);
+        } catch (ex) {
+            callback(ex);
+        }
+    }
+    /**
+     * returns the file coverage object for the last file instrumented.
+     * @returns {Object} the file coverage object.
+     */
+    lastFileCoverage() {
+        return this.fileCoverage;
+    }
+    /**
+     * returns the source map produced for the last file instrumented.
+     * @returns {null|Object} the source map object.
+     */
+    lastSourceMap() {
+        return this.sourceMap;
+    }
+}
+
+module.exports = Instrumenter;
Index: frontend/node_modules/istanbul-lib-instrument/src/read-coverage.js
===================================================================
--- frontend/node_modules/istanbul-lib-instrument/src/read-coverage.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/istanbul-lib-instrument/src/read-coverage.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,77 @@
+const { parseSync, traverse } = require('@babel/core');
+const { defaults } = require('@istanbuljs/schema');
+const { MAGIC_KEY, MAGIC_VALUE } = require('./constants');
+
+function getAst(code) {
+    if (typeof code === 'object' && typeof code.type === 'string') {
+        // Assume code is already a babel ast.
+        return code;
+    }
+
+    if (typeof code !== 'string') {
+        throw new Error('Code must be a string');
+    }
+
+    // Parse as leniently as possible
+    return parseSync(code, {
+        babelrc: false,
+        configFile: false,
+        parserOpts: {
+            allowAwaitOutsideFunction: true,
+            allowImportExportEverywhere: true,
+            allowReturnOutsideFunction: true,
+            allowSuperOutsideMethod: true,
+            sourceType: 'script',
+            plugins: defaults.instrumenter.parserPlugins
+        }
+    });
+}
+
+module.exports = function readInitialCoverage(code) {
+    const ast = getAst(code);
+
+    let covScope;
+    traverse(ast, {
+        ObjectProperty(path) {
+            const { node } = path;
+            if (
+                !node.computed &&
+                path.get('key').isIdentifier() &&
+                node.key.name === MAGIC_KEY
+            ) {
+                const magicValue = path.get('value').evaluate();
+                if (!magicValue.confident || magicValue.value !== MAGIC_VALUE) {
+                    return;
+                }
+                covScope =
+                    path.scope.getFunctionParent() ||
+                    path.scope.getProgramParent();
+                path.stop();
+            }
+        }
+    });
+
+    if (!covScope) {
+        return null;
+    }
+
+    const result = {};
+
+    for (const key of ['path', 'hash', 'gcv', 'coverageData']) {
+        const binding = covScope.getOwnBinding(key);
+        if (!binding) {
+            return null;
+        }
+        const valuePath = binding.path.get('init');
+        const value = valuePath.evaluate();
+        if (!value.confident) {
+            return null;
+        }
+        result[key] = value.value;
+    }
+
+    delete result.coverageData[MAGIC_KEY];
+    delete result.coverageData.hash;
+
+    return result;
+};
Index: frontend/node_modules/istanbul-lib-instrument/src/source-coverage.js
===================================================================
--- frontend/node_modules/istanbul-lib-instrument/src/source-coverage.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/istanbul-lib-instrument/src/source-coverage.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,135 @@
+const { classes } = require('istanbul-lib-coverage');
+
+function cloneLocation(loc) {
+    return {
+        start: {
+            line: loc && loc.start.line,
+            column: loc && loc.start.column
+        },
+        end: {
+            line: loc && loc.end.line,
+            column: loc && loc.end.column
+        }
+    };
+}
+/**
+ * SourceCoverage provides mutation methods to manipulate the structure of
+ * a file coverage object. Used by the instrumenter to create a full coverage
+ * object for a file incrementally.
+ *
+ * @private
+ * @param pathOrObj {String|Object} - see the argument for {@link FileCoverage}
+ * @extends FileCoverage
+ * @constructor
+ */
+class SourceCoverage extends classes.FileCoverage {
+    constructor(pathOrObj) {
+        super(pathOrObj);
+        this.meta = {
+            last: {
+                s: 0,
+                f: 0,
+                b: 0
+            }
+        };
+    }
+
+    newStatement(loc) {
+        const s = this.meta.last.s;
+        this.data.statementMap[s] = cloneLocation(loc);
+        this.data.s[s] = 0;
+        this.meta.last.s += 1;
+        return s;
+    }
+
+    newFunction(name, decl, loc) {
+        const f = this.meta.last.f;
+        name = name || '(anonymous_' + f + ')';
+        this.data.fnMap[f] = {
+            name,
+            decl: cloneLocation(decl),
+            loc: cloneLocation(loc),
+            // DEPRECATED: some legacy reports require this info.
+            line: loc && loc.start.line
+        };
+        this.data.f[f] = 0;
+        this.meta.last.f += 1;
+        return f;
+    }
+
+    newBranch(type, loc, isReportLogic = false) {
+        const b = this.meta.last.b;
+        this.data.b[b] = [];
+        this.data.branchMap[b] = {
+            loc: cloneLocation(loc),
+            type,
+            locations: [],
+            // DEPRECATED: some legacy reports require this info.
+            line: loc && loc.start.line
+        };
+        this.meta.last.b += 1;
+        this.maybeNewBranchTrue(type, b, isReportLogic);
+        return b;
+    }
+
+    maybeNewBranchTrue(type, name, isReportLogic) {
+        if (!isReportLogic) {
+            return;
+        }
+        if (type !== 'binary-expr') {
+            return;
+        }
+        this.data.bT = this.data.bT || {};
+        this.data.bT[name] = [];
+    }
+
+    addBranchPath(name, location) {
+        const bMeta = this.data.branchMap[name];
+        const counts = this.data.b[name];
+
+        /* istanbul ignore if: paranoid check */
+        if (!bMeta) {
+            throw new Error('Invalid branch ' + name);
+        }
+        bMeta.locations.push(cloneLocation(location));
+        counts.push(0);
+        this.maybeAddBranchTrue(name);
+        return counts.length - 1;
+    }
+
+    maybeAddBranchTrue(name) {
+        if (!this.data.bT) {
+            return;
+        }
+        const countsTrue = this.data.bT[name];
+        if (!countsTrue) {
+            return;
+        }
+        countsTrue.push(0);
+    }
+
+    /**
+     * Assigns an input source map to the coverage that can be used
+     * to remap the coverage output to the original source
+     * @param sourceMap {object} the source map
+     */
+    inputSourceMap(sourceMap) {
+        this.data.inputSourceMap = sourceMap;
+    }
+
+    freeze() {
+        // prune empty branches
+        const map = this.data.branchMap;
+        const branches = this.data.b;
+        const branchesT = this.data.bT || {};
+        Object.keys(map).forEach(b => {
+            if (map[b].locations.length === 0) {
+                delete map[b];
+                delete branches[b];
+                delete branchesT[b];
+            }
+        });
+    }
+}
+
+module.exports = { SourceCoverage };
Index: frontend/node_modules/istanbul-lib-instrument/src/visitor.js
===================================================================
--- frontend/node_modules/istanbul-lib-instrument/src/visitor.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/istanbul-lib-instrument/src/visitor.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,843 @@
+const { createHash } = require('crypto');
+const { template } = require('@babel/core');
+const { defaults } = require('@istanbuljs/schema');
+const { SourceCoverage } = require('./source-coverage');
+const { SHA, MAGIC_KEY, MAGIC_VALUE } = require('./constants');
+
+// pattern for istanbul to ignore a section
+const COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/;
+// pattern for istanbul to ignore the whole file
+const COMMENT_FILE_RE = /^\s*istanbul\s+ignore\s+(file)(?=\W|$)/;
+// source map URL pattern
+const SOURCE_MAP_RE = /[#@]\s*sourceMappingURL=(.*)\s*$/m;
+
+// generate a variable name from hashing the supplied file path
+function genVar(filename) {
+    const hash = createHash(SHA);
+    hash.update(filename);
+    return 'cov_' + parseInt(hash.digest('hex').substr(0, 12), 16).toString(36);
+}
+
+// VisitState holds the state of the visitor, provides helper functions
+// and is the `this` for the individual coverage visitors.
+class VisitState {
+    constructor(
+        types,
+        sourceFilePath,
+        inputSourceMap,
+        ignoreClassMethods = [],
+        reportLogic = false
+    ) {
+        this.varName = genVar(sourceFilePath);
+        this.attrs = {};
+        this.nextIgnore = null;
+        this.cov = new SourceCoverage(sourceFilePath);
+
+        if (typeof inputSourceMap !== 'undefined') {
+            this.cov.inputSourceMap(inputSourceMap);
+        }
+        this.ignoreClassMethods = ignoreClassMethods;
+        this.types = types;
+        this.sourceMappingURL = null;
+        this.reportLogic = reportLogic;
+    }
+
+    // should we ignore the node? Yes, if specifically ignoring
+    // or if the node is generated.
+    shouldIgnore(path) {
+        return this.nextIgnore || !path.node.loc;
+    }
+
+    // extract the ignore comment hint (next|if|else) or null
+    hintFor(node) {
+        let hint = null;
+        if (node.leadingComments) {
+            node.leadingComments.forEach(c => {
+                const v = (
+                    c.value || /* istanbul ignore next: paranoid check */ ''
+                ).trim();
+                const groups = v.match(COMMENT_RE);
+                if (groups) {
+                    hint = groups[1];
+                }
+            });
+        }
+        return hint;
+    }
+
+    // extract a source map URL from comments and keep track of it
+    maybeAssignSourceMapURL(node) {
+        const extractURL = comments => {
+            if (!comments) {
+                return;
+            }
+            comments.forEach(c => {
+                const v = (
+                    c.value || /* istanbul ignore next: paranoid check */ ''
+                ).trim();
+                const groups = v.match(SOURCE_MAP_RE);
+                if (groups) {
+                    this.sourceMappingURL = groups[1];
+                }
+            });
+        };
+        extractURL(node.leadingComments);
+        extractURL(node.trailingComments);
+    }
+
+    // for these expressions the statement counter needs to be hoisted, so
+    // function name inference can be preserved
+    counterNeedsHoisting(path) {
+        return (
+            path.isFunctionExpression() ||
+            path.isArrowFunctionExpression() ||
+            path.isClassExpression()
+        );
+    }
+
+    // all the generic stuff that needs to be done on enter for every node
+    onEnter(path) {
+        const n = path.node;
+
+        this.maybeAssignSourceMapURL(n);
+
+        // if already ignoring, nothing more to do
+        if (this.nextIgnore !== null) {
+            return;
+        }
+        // check hint to see if ignore should be turned on
+        const hint = this.hintFor(n);
+        if (hint === 'next') {
+            this.nextIgnore = n;
+            return;
+        }
+        // else check custom node attribute set by a prior visitor
+        if (this.getAttr(path.node, 'skip-all') !== null) {
+            this.nextIgnore = n;
+        }
+
+        // else check for ignored class methods
+        if (
+            path.isFunctionExpression() &&
+            this.ignoreClassMethods.some(
+                name => path.node.id && name === path.node.id.name
+            )
+        ) {
+            this.nextIgnore = n;
+            return;
+        }
+        if (
+            path.isClassMethod() &&
+            this.ignoreClassMethods.some(name => name === path.node.key.name)
+        ) {
+            this.nextIgnore = n;
+            return;
+        }
+    }
+
+    // all the generic stuff on exit of a node,
+    // including reseting ignores and custom node attrs
+    onExit(path) {
+        // restore ignore status, if needed
+        if (path.node === this.nextIgnore) {
+            this.nextIgnore = null;
+        }
+        // nuke all attributes for the node
+        delete path.node.__cov__;
+    }
+
+    // set a node attribute for the supplied node
+    setAttr(node, name, value) {
+        node.__cov__ = node.__cov__ || {};
+        node.__cov__[name] = value;
+    }
+
+    // retrieve a node attribute for the supplied node or null
+    getAttr(node, name) {
+        const c = node.__cov__;
+        if (!c) {
+            return null;
+        }
+        return c[name];
+    }
+
+    //
+    increase(type, id, index) {
+        const T = this.types;
+        const wrap =
+            index !== null
+                ? // If `index` present, turn `x` into `x[index]`.
+                  x => T.memberExpression(x, T.numericLiteral(index), true)
+                : x => x;
+        return T.updateExpression(
+            '++',
+            wrap(
+                T.memberExpression(
+                    T.memberExpression(
+                        T.callExpression(T.identifier(this.varName), []),
+                        T.identifier(type)
+                    ),
+                    T.numericLiteral(id),
+                    true
+                )
+            )
+        );
+    }
+
+    // Reads the logic expression conditions and conditionally increments truthy counter.
+    increaseTrue(type, id, index, node) {
+        const T = this.types;
+        const tempName = `${this.varName}_temp`;
+
+        return T.sequenceExpression([
+            T.assignmentExpression(
+                '=',
+                T.memberExpression(
+                    T.callExpression(T.identifier(this.varName), []),
+                    T.identifier(tempName)
+                ),
+                node // Only evaluates once.
+            ),
+            T.parenthesizedExpression(
+                T.conditionalExpression(
+                    this.validateTrueNonTrivial(T, tempName),
+                    this.increase(type, id, index),
+                    T.nullLiteral()
+                )
+            ),
+            T.memberExpression(
+                T.callExpression(T.identifier(this.varName), []),
+                T.identifier(tempName)
+            )
+        ]);
+    }
+
+    validateTrueNonTrivial(T, tempName) {
+        return T.logicalExpression(
+            '&&',
+            T.memberExpression(
+                T.callExpression(T.identifier(this.varName), []),
+                T.identifier(tempName)
+            ),
+            T.logicalExpression(
+                '&&',
+                T.parenthesizedExpression(
+                    T.logicalExpression(
+                        '||',
+                        T.unaryExpression(
+                            '!',
+                            T.callExpression(
+                                T.memberExpression(
+                                    T.identifier('Array'),
+                                    T.identifier('isArray')
+                                ),
+                                [
+                                    T.memberExpression(
+                                        T.callExpression(
+                                            T.identifier(this.varName),
+                                            []
+                                        ),
+                                        T.identifier(tempName)
+                                    )
+                                ]
+                            )
+                        ),
+                        T.memberExpression(
+                            T.memberExpression(
+                                T.callExpression(
+                                    T.identifier(this.varName),
+                                    []
+                                ),
+                                T.identifier(tempName)
+                            ),
+                            T.identifier('length')
+                        )
+                    )
+                ),
+                T.parenthesizedExpression(
+                    T.logicalExpression(
+                        '||',
+                        T.binaryExpression(
+                            '!==',
+                            T.callExpression(
+                                T.memberExpression(
+                                    T.identifier('Object'),
+                                    T.identifier('getPrototypeOf')
+                                ),
+                                [
+                                    T.memberExpression(
+                                        T.callExpression(
+                                            T.identifier(this.varName),
+                                            []
+                                        ),
+                                        T.identifier(tempName)
+                                    )
+                                ]
+                            ),
+                            T.memberExpression(
+                                T.identifier('Object'),
+                                T.identifier('prototype')
+                            )
+                        ),
+                        T.memberExpression(
+                            T.callExpression(
+                                T.memberExpression(
+                                    T.identifier('Object'),
+                                    T.identifier('values')
+                                ),
+                                [
+                                    T.memberExpression(
+                                        T.callExpression(
+                                            T.identifier(this.varName),
+                                            []
+                                        ),
+                                        T.identifier(tempName)
+                                    )
+                                ]
+                            ),
+                            T.identifier('length')
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    insertCounter(path, increment) {
+        const T = this.types;
+        if (path.isBlockStatement()) {
+            path.node.body.unshift(T.expressionStatement(increment));
+        } else if (path.isStatement()) {
+            path.insertBefore(T.expressionStatement(increment));
+        } else if (
+            this.counterNeedsHoisting(path) &&
+            T.isVariableDeclarator(path.parentPath)
+        ) {
+            // make an attempt to hoist the statement counter, so that
+            // function names are maintained.
+            const parent = path.parentPath.parentPath;
+            if (parent && T.isExportNamedDeclaration(parent.parentPath)) {
+                parent.parentPath.insertBefore(
+                    T.expressionStatement(increment)
+                );
+            } else if (
+                parent &&
+                (T.isProgram(parent.parentPath) ||
+                    T.isBlockStatement(parent.parentPath))
+            ) {
+                parent.insertBefore(T.expressionStatement(increment));
+            } else {
+                path.replaceWith(T.sequenceExpression([increment, path.node]));
+            }
+        } /* istanbul ignore else: not expected */ else if (
+            path.isExpression()
+        ) {
+            path.replaceWith(T.sequenceExpression([increment, path.node]));
+        } else {
+            console.error(
+                'Unable to insert counter for node type:',
+                path.node.type
+            );
+        }
+    }
+
+    insertStatementCounter(path) {
+        /* istanbul ignore if: paranoid check */
+        if (!(path.node && path.node.loc)) {
+            return;
+        }
+        const index = this.cov.newStatement(path.node.loc);
+        const increment = this.increase('s', index, null);
+        this.insertCounter(path, increment);
+    }
+
+    insertFunctionCounter(path) {
+        const T = this.types;
+        /* istanbul ignore if: paranoid check */
+        if (!(path.node && path.node.loc)) {
+            return;
+        }
+        const n = path.node;
+
+        let dloc = null;
+        // get location for declaration
+        switch (n.type) {
+            case 'FunctionDeclaration':
+            case 'FunctionExpression':
+                /* istanbul ignore else: paranoid check */
+                if (n.id) {
+                    dloc = n.id.loc;
+                }
+                break;
+        }
+        if (!dloc) {
+            dloc = {
+                start: n.loc.start,
+                end: { line: n.loc.start.line, column: n.loc.start.column + 1 }
+            };
+        }
+
+        const name = path.node.id ? path.node.id.name : path.node.name;
+        const index = this.cov.newFunction(name, dloc, path.node.body.loc);
+        const increment = this.increase('f', index, null);
+        const body = path.get('body');
+        /* istanbul ignore else: not expected */
+        if (body.isBlockStatement()) {
+            body.node.body.unshift(T.expressionStatement(increment));
+        } else {
+            console.error(
+                'Unable to process function body node type:',
+                path.node.type
+            );
+        }
+    }
+
+    getBranchIncrement(branchName, loc) {
+        const index = this.cov.addBranchPath(branchName, loc);
+        return this.increase('b', branchName, index);
+    }
+
+    getBranchLogicIncrement(path, branchName, loc) {
+        const index = this.cov.addBranchPath(branchName, loc);
+        return [
+            this.increase('b', branchName, index),
+            this.increaseTrue('bT', branchName, index, path.node)
+        ];
+    }
+
+    insertBranchCounter(path, branchName, loc) {
+        const increment = this.getBranchIncrement(
+            branchName,
+            loc || path.node.loc
+        );
+        this.insertCounter(path, increment);
+    }
+
+    findLeaves(node, accumulator, parent, property) {
+        if (!node) {
+            return;
+        }
+        if (node.type === 'LogicalExpression') {
+            const hint = this.hintFor(node);
+            if (hint !== 'next') {
+                this.findLeaves(node.left, accumulator, node, 'left');
+                this.findLeaves(node.right, accumulator, node, 'right');
+            }
+        } else {
+            accumulator.push({
+                node,
+                parent,
+                property
+            });
+        }
+    }
+}
+
+// generic function that takes a set of visitor methods and
+// returns a visitor object with `enter` and `exit` properties,
+// such that:
+//
+// * standard entry processing is done
+// * the supplied visitors are called only when ignore is not in effect
+//   This relieves them from worrying about ignore states and generated nodes.
+// * standard exit processing is done
+//
+function entries(...enter) {
+    // the enter function
+    const wrappedEntry = function(path, node) {
+        this.onEnter(path);
+        if (this.shouldIgnore(path)) {
+            return;
+        }
+        enter.forEach(e => {
+            e.call(this, path, node);
+        });
+    };
+    const exit = function(path, node) {
+        this.onExit(path, node);
+    };
+    return {
+        enter: wrappedEntry,
+        exit
+    };
+}
+
+function coverStatement(path) {
+    this.insertStatementCounter(path);
+}
+
+/* istanbul ignore next: no node.js support */
+function coverAssignmentPattern(path) {
+    const n = path.node;
+    const b = this.cov.newBranch('default-arg', n.loc);
+    this.insertBranchCounter(path.get('right'), b);
+}
+
+function coverFunction(path) {
+    this.insertFunctionCounter(path);
+}
+
+function coverVariableDeclarator(path) {
+    this.insertStatementCounter(path.get('init'));
+}
+
+function coverClassPropDeclarator(path) {
+    this.insertStatementCounter(path.get('value'));
+}
+
+function makeBlock(path) {
+    const T = this.types;
+    if (!path.node) {
+        path.replaceWith(T.blockStatement([]));
+    }
+    if (!path.isBlockStatement()) {
+        path.replaceWith(T.blockStatement([path.node]));
+        path.node.loc = path.node.body[0].loc;
+        path.node.body[0].leadingComments = path.node.leadingComments;
+        path.node.leadingComments = undefined;
+    }
+}
+
+function blockProp(prop) {
+    return function(path) {
+        makeBlock.call(this, path.get(prop));
+    };
+}
+
+function makeParenthesizedExpressionForNonIdentifier(path) {
+    const T = this.types;
+    if (path.node && !path.isIdentifier()) {
+        path.replaceWith(T.parenthesizedExpression(path.node));
+    }
+}
+
+function parenthesizedExpressionProp(prop) {
+    return function(path) {
+        makeParenthesizedExpressionForNonIdentifier.call(this, path.get(prop));
+    };
+}
+
+function convertArrowExpression(path) {
+    const n = path.node;
+    const T = this.types;
+    if (!T.isBlockStatement(n.body)) {
+        const bloc = n.body.loc;
+        if (n.expression === true) {
+            n.expression = false;
+        }
+        n.body = T.blockStatement([T.returnStatement(n.body)]);
+        // restore body location
+        n.body.loc = bloc;
+        // set up the location for the return statement so it gets
+        // instrumented
+        n.body.body[0].loc = bloc;
+    }
+}
+
+function coverIfBranches(path) {
+    const n = path.node;
+    const hint = this.hintFor(n);
+    const ignoreIf = hint === 'if';
+    const ignoreElse = hint === 'else';
+    const branch = this.cov.newBranch('if', n.loc);
+
+    if (ignoreIf) {
+        this.setAttr(n.consequent, 'skip-all', true);
+    } else {
+        this.insertBranchCounter(path.get('consequent'), branch, n.loc);
+    }
+    if (ignoreElse) {
+        this.setAttr(n.alternate, 'skip-all', true);
+    } else {
+        this.insertBranchCounter(path.get('alternate'), branch);
+    }
+}
+
+function createSwitchBranch(path) {
+    const b = this.cov.newBranch('switch', path.node.loc);
+    this.setAttr(path.node, 'branchName', b);
+}
+
+function coverSwitchCase(path) {
+    const T = this.types;
+    const b = this.getAttr(path.parentPath.node, 'branchName');
+    /* istanbul ignore if: paranoid check */
+    if (b === null) {
+        throw new Error('Unable to get switch branch name');
+    }
+    const increment = this.getBranchIncrement(b, path.node.loc);
+    path.node.consequent.unshift(T.expressionStatement(increment));
+}
+
+function coverTernary(path) {
+    const n = path.node;
+    const branch = this.cov.newBranch('cond-expr', path.node.loc);
+    const cHint = this.hintFor(n.consequent);
+    const aHint = this.hintFor(n.alternate);
+
+    if (cHint !== 'next') {
+        this.insertBranchCounter(path.get('consequent'), branch);
+    }
+    if (aHint !== 'next') {
+        this.insertBranchCounter(path.get('alternate'), branch);
+    }
+}
+
+function coverLogicalExpression(path) {
+    const T = this.types;
+    if (path.parentPath.node.type === 'LogicalExpression') {
+        return; // already processed
+    }
+    const leaves = [];
+    this.findLeaves(path.node, leaves);
+    const b = this.cov.newBranch(
+        'binary-expr',
+        path.node.loc,
+        this.reportLogic
+    );
+    for (let i = 0; i < leaves.length; i += 1) {
+        const leaf = leaves[i];
+        const hint = this.hintFor(leaf.node);
+        if (hint === 'next') {
+            continue;
+        }
+
+        if (this.reportLogic) {
+            const increment = this.getBranchLogicIncrement(
+                leaf,
+                b,
+                leaf.node.loc
+            );
+            if (!increment[0]) {
+                continue;
+            }
+            leaf.parent[leaf.property] = T.sequenceExpression([
+                increment[0],
+                increment[1]
+            ]);
+            continue;
+        }
+
+        const increment = this.getBranchIncrement(b, leaf.node.loc);
+        if (!increment) {
+            continue;
+        }
+        leaf.parent[leaf.property] = T.sequenceExpression([
+            increment,
+            leaf.node
+        ]);
+    }
+}
+
+const codeVisitor = {
+    ArrowFunctionExpression: entries(convertArrowExpression, coverFunction),
+    AssignmentPattern: entries(coverAssignmentPattern),
+    BlockStatement: entries(), // ignore processing only
+    ExportDefaultDeclaration: entries(), // ignore processing only
+    ExportNamedDeclaration: entries(), // ignore processing only
+    ClassMethod: entries(coverFunction),
+    ClassDeclaration: entries(parenthesizedExpressionProp('superClass')),
+    ClassProperty: entries(coverClassPropDeclarator),
+    ClassPrivateProperty: entries(coverClassPropDeclarator),
+    ObjectMethod: entries(coverFunction),
+    ExpressionStatement: entries(coverStatement),
+    BreakStatement: entries(coverStatement),
+    ContinueStatement: entries(coverStatement),
+    DebuggerStatement: entries(coverStatement),
+    ReturnStatement: entries(coverStatement),
+    ThrowStatement: entries(coverStatement),
+    TryStatement: entries(coverStatement),
+    VariableDeclaration: entries(), // ignore processing only
+    VariableDeclarator: entries(coverVariableDeclarator),
+    IfStatement: entries(
+        blockProp('consequent'),
+        blockProp('alternate'),
+        coverStatement,
+        coverIfBranches
+    ),
+    ForStatement: entries(blockProp('body'), coverStatement),
+    ForInStatement: entries(blockProp('body'), coverStatement),
+    ForOfStatement: entries(blockProp('body'), coverStatement),
+    WhileStatement: entries(blockProp('body'), coverStatement),
+    DoWhileStatement: entries(blockProp('body'), coverStatement),
+    SwitchStatement: entries(createSwitchBranch, coverStatement),
+    SwitchCase: entries(coverSwitchCase),
+    WithStatement: entries(blockProp('body'), coverStatement),
+    FunctionDeclaration: entries(coverFunction),
+    FunctionExpression: entries(coverFunction),
+    LabeledStatement: entries(coverStatement),
+    ConditionalExpression: entries(coverTernary),
+    LogicalExpression: entries(coverLogicalExpression)
+};
+const globalTemplateAlteredFunction = template(`
+        var Function = (function(){}).constructor;
+        var global = (new Function(GLOBAL_COVERAGE_SCOPE))();
+`);
+const globalTemplateFunction = template(`
+        var global = (new Function(GLOBAL_COVERAGE_SCOPE))();
+`);
+const globalTemplateVariable = template(`
+        var global = GLOBAL_COVERAGE_SCOPE;
+`);
+// the template to insert at the top of the program.
+const coverageTemplate = template(
+    `
+    function COVERAGE_FUNCTION () {
+        var path = PATH;
+        var hash = HASH;
+        GLOBAL_COVERAGE_TEMPLATE
+        var gcv = GLOBAL_COVERAGE_VAR;
+        var coverageData = INITIAL;
+        var coverage = global[gcv] || (global[gcv] = {});
+        if (!coverage[path] || coverage[path].hash !== hash) {
+            coverage[path] = coverageData;
+        }
+
+        var actualCoverage = coverage[path];
+        {
+            // @ts-ignore
+            COVERAGE_FUNCTION = function () {
+                return actualCoverage;
+            }
+        }
+
+        return actualCoverage;
+    }
+`,
+    { preserveComments: true }
+);
+// the rewire plugin (and potentially other babel middleware)
+// may cause files to be instrumented twice, see:
+// https://github.com/istanbuljs/babel-plugin-istanbul/issues/94
+// we should only instrument code for coverage the first time
+// it's run through istanbul-lib-instrument.
+function alreadyInstrumented(path, visitState) {
+    return path.scope.hasBinding(visitState.varName);
+}
+function shouldIgnoreFile(programNode) {
+    return (
+        programNode.parent &&
+        programNode.parent.comments.some(c => COMMENT_FILE_RE.test(c.value))
+    );
+}
+
+/**
+ * programVisitor is a `babel` adaptor for instrumentation.
+ * It returns an object with two methods `enter` and `exit`.
+ * These should be assigned to or called from `Program` entry and exit functions
+ * in a babel visitor.
+ * These functions do not make assumptions about the state set by Babel and thus
+ * can be used in a context other than a Babel plugin.
+ *
+ * The exit function returns an object that currently has the following keys:
+ *
+ * `fileCoverage` - the file coverage object created for the source file.
+ * `sourceMappingURL` - any source mapping URL found when processing the file.
+ *
+ * @param {Object} types - an instance of babel-types.
+ * @param {string} sourceFilePath - the path to source file.
+ * @param {Object} opts - additional options.
+ * @param {string} [opts.coverageVariable=__coverage__] the global coverage variable name.
+ * @param {boolean} [opts.reportLogic=false] report boolean value of logical expressions.
+ * @param {string} [opts.coverageGlobalScope=this] the global coverage variable scope.
+ * @param {boolean} [opts.coverageGlobalScopeFunc=true] use an evaluated function to find coverageGlobalScope.
+ * @param {Array} [opts.ignoreClassMethods=[]] names of methods to ignore by default on classes.
+ * @param {object} [opts.inputSourceMap=undefined] the input source map, that maps the uninstrumented code back to the
+ * original code.
+ */
+function programVisitor(types, sourceFilePath = 'unknown.js', opts = {}) {
+    const T = types;
+    opts = {
+        ...defaults.instrumentVisitor,
+        ...opts
+    };
+    const visitState = new VisitState(
+        types,
+        sourceFilePath,
+        opts.inputSourceMap,
+        opts.ignoreClassMethods,
+        opts.reportLogic
+    );
+    return {
+        enter(path) {
+            if (shouldIgnoreFile(path.find(p => p.isProgram()))) {
+                return;
+            }
+            if (alreadyInstrumented(path, visitState)) {
+                return;
+            }
+            path.traverse(codeVisitor, visitState);
+        },
+        exit(path) {
+            if (alreadyInstrumented(path, visitState)) {
+                return;
+            }
+            visitState.cov.freeze();
+            const coverageData = visitState.cov.toJSON();
+            if (shouldIgnoreFile(path.find(p => p.isProgram()))) {
+                return {
+                    fileCoverage: coverageData,
+                    sourceMappingURL: visitState.sourceMappingURL
+                };
+            }
+            coverageData[MAGIC_KEY] = MAGIC_VALUE;
+            const hash = createHash(SHA)
+                .update(JSON.stringify(coverageData))
+                .digest('hex');
+            coverageData.hash = hash;
+            if (
+                coverageData.inputSourceMap &&
+                Object.getPrototypeOf(coverageData.inputSourceMap) !==
+                    Object.prototype
+            ) {
+                coverageData.inputSourceMap = {
+                    ...coverageData.inputSourceMap
+                };
+            }
+            const coverageNode = T.valueToNode(coverageData);
+            delete coverageData[MAGIC_KEY];
+            delete coverageData.hash;
+            let gvTemplate;
+            if (opts.coverageGlobalScopeFunc) {
+                if (path.scope.getBinding('Function')) {
+                    gvTemplate = globalTemplateAlteredFunction({
+                        GLOBAL_COVERAGE_SCOPE: T.stringLiteral(
+                            'return ' + opts.coverageGlobalScope
+                        )
+                    });
+                } else {
+                    gvTemplate = globalTemplateFunction({
+                        GLOBAL_COVERAGE_SCOPE: T.stringLiteral(
+                            'return ' + opts.coverageGlobalScope
+                        )
+                    });
+                }
+            } else {
+                gvTemplate = globalTemplateVariable({
+                    GLOBAL_COVERAGE_SCOPE: opts.coverageGlobalScope
+                });
+            }
+            const cv = coverageTemplate({
+                GLOBAL_COVERAGE_VAR: T.stringLiteral(opts.coverageVariable),
+                GLOBAL_COVERAGE_TEMPLATE: gvTemplate,
+                COVERAGE_FUNCTION: T.identifier(visitState.varName),
+                PATH: T.stringLiteral(sourceFilePath),
+                INITIAL: coverageNode,
+                HASH: T.stringLiteral(hash)
+            });
+            // explicitly call this.varName to ensure coverage is always initialized
+            path.node.body.unshift(
+                T.expressionStatement(
+                    T.callExpression(T.identifier(visitState.varName), [])
+                )
+            );
+            path.node.body.unshift(cv);
+            return {
+                fileCoverage: coverageData,
+                sourceMappingURL: visitState.sourceMappingURL
+            };
+        }
+    };
+}
+
+module.exports = programVisitor;
