static makeScriptNode(src, sourceRoot, sourcesData) {
    /**
     * @param {string} name
     * @return {LH.Treemap.Node}
     */
    function newNode(name) {
      return {
        name,
        resourceBytes: 0,
        encodedBytes: undefined,
      };
    }

    const sourceRootNode = newNode(sourceRoot);

    /**
     * Given a slash-delimited path, traverse the Node structure and increment
     * the data provided for each node in the chain. Creates nodes as needed.
     * Ex: path/to/file.js will find or create "path" on `node`, increment the data fields,
     *     and continue with "to", and so on.
     * @param {string} source
     * @param {SourceData} data
     */
    function addAllNodesInSourcePath(source, data) {
      let node = sourceRootNode;

      // Apply the data to the sourceRootNode.
      sourceRootNode.resourceBytes += data.resourceBytes;
      if (data.unusedBytes) {
        sourceRootNode.unusedBytes = (sourceRootNode.unusedBytes || 0) + data.unusedBytes;
      }

      // Strip off the shared root.
      const sourcePathSegments = source.replace(sourceRoot, '').split(/\/+/);
      sourcePathSegments.forEach((sourcePathSegment, i) => {
        if (sourcePathSegment.length === 0) return;

        const isLeaf = i === sourcePathSegments.length - 1;

        let child = node.children && node.children.find(child => child.name === sourcePathSegment);
        if (!child) {
          child = newNode(sourcePathSegment);
          node.children = node.children || [];
          node.children.push(child);
        }
        node = child;

        // Now that we've found or created the next node in the path, apply the data.
        node.resourceBytes += data.resourceBytes;
        if (data.unusedBytes) node.unusedBytes = (node.unusedBytes || 0) + data.unusedBytes;

        // Only leaf nodes might have duplication data.
        if (isLeaf && data.duplicatedNormalizedModuleName !== undefined) {
          node.duplicatedNormalizedModuleName = data.duplicatedNormalizedModuleName;
        }
      });
    }

    // For every source file, apply the data to all components
    // of the source path, creating nodes as necessary.
    for (const [source, data] of Object.entries(sourcesData)) {
      addAllNodesInSourcePath(source, data);
    }

    /**
     * Collapse nodes that have only one child.
     * @param {LH.Treemap.Node} node
     */
    function collapseAll(node) {
      while (node.children && node.children.length === 1) {
        const child = node.children[0];
        node.name += '/' + child.name;
        if (child.duplicatedNormalizedModuleName) {
          node.duplicatedNormalizedModuleName = child.duplicatedNormalizedModuleName;
        }
        node.children = child.children;
      }

      if (node.children) {
        for (const child of node.children) {
          collapseAll(child);
        }
      }
    }
    collapseAll(sourceRootNode);

    // If sourceRootNode.name is falsy (no defined sourceRoot + no collapsed common prefix),
    // collapse the sourceRootNode children into the scriptNode.
    // Otherwise, we add another node.
    if (!sourceRootNode.name) {
      return {
        ...sourceRootNode,
        name: src,
        children: sourceRootNode.children,
      };
    }

    // Script node should be just the script src.
    const scriptNode = {...sourceRootNode};
    scriptNode.name = src;
    scriptNode.children = [sourceRootNode];
    return scriptNode;
  }
Returns a tree data structure where leaf nodes are sources (ie. real files from source tree)
from a source map, and non-leaf nodes are directories. Leaf nodes have data
for bytes, coverage, etc., when available, and non-leaf nodes have the
same data as the sum of all descendant leaf nodes.
@param {string} src
@param {string} sourceRoot
@param {Record<string, SourceData>} sourcesData
@return {LH.Treemap.Node}