function getNodePath(node) {
  // For our purposes, there's no worthwhile difference between shadow root and document fragment
  // We can consider them entirely synonymous.
  /** @param {Node} node @return {node is ShadowRoot} */
  const isShadowRoot = node => node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
  /** @param {Node} node */
  const getNodeParent = node => isShadowRoot(node) ? node.host : node.parentNode;

  /** @param {Node} node @return {number|'a'} */
  function getNodeIndex(node) {
    if (isShadowRoot(node)) {
      // User-agent shadow roots get 'u'. Non-UA shadow roots get 'a'.
      return 'a';
    }
    let index = 0;
    let prevNode;
    while (prevNode = node.previousSibling) { // eslint-disable-line no-cond-assign
      node = prevNode;
      // skip empty text nodes
      if (node.nodeType === Node.TEXT_NODE && (node.nodeValue || '').trim().length === 0) continue;
      index++;
    }
    return index;
  }

  /** @type {Node|null} */
  let currentNode = node;
  const path = [];
  while (currentNode && getNodeParent(currentNode)) {
    const index = getNodeIndex(currentNode);
    path.push([index, currentNode.nodeName]);
    currentNode = getNodeParent(currentNode);
  }
  path.reverse();
  return path.join(',');
}
Adapted from DevTools' SDK.DOMNode.prototype.path
  https://github.com/ChromeDevTools/devtools-frontend/blob/4fff931bb/front_end/sdk/DOMModel.js#L625-L647
Backend: https://source.chromium.org/search?q=f:node.cc%20symbol:PrintNodePathTo&sq=&ss=chromium%2Fchromium%2Fsrc

TODO: DevTools nodePath handling doesn't support iframes, but probably could. https://crbug.com/1127635
@param {Node} node
@return {string}