Merge changes I3a17b387,I872bc926,I40692a07,I66a3cb81,I8fb9e8dc, ...

* changes:
  Get rid of some global variables - Part 4
  Get rid of some global variables - Part 3
  Get rid of some global variables - Part 2
  Get rid of global GrEtagDecorator
  Get rid of global GrDomHooksManager and GrDomHook
  Get rid of some global variables - Part 1
  Get rid of global GrChangeActionsInterface
  Get rid of global GrDiffBuilderBinary
  Get rid of global GrDiffBuilderUnified
  Get rid of global GrDiffBuilderImage
  Get rid of global GrDiffBuilderSideBySide
  Get rid of global GrDiffBuilder
  Get rid of global GrDiffGroup
  Get rid of global GrDiffLine
  Get rid of global GrAttributeHelper
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index b8e381b..593a2f8 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -167,48 +167,22 @@
     // TODO(dmfilippov): Remove global variables from polygerrit
     "Auth": "readonly",
     "EventEmitter": "readonly",
-    "FetchPromisesCache": "readonly",
     "Gerrit": "readonly",
     "GrAdminApi": "readonly",
     "GrAnnotationActionsContext": "readonly",
     "GrAnnotationActionsInterface": "readonly",
-    "GrAttributeHelper": "readonly",
-    "GrChangeActionsInterface": "readonly",
     "GrChangeMetadataApi": "readonly",
-    "GrChangeReplyInterface": "readonly",
     "GrChangeViewApi": "readonly",
-    "GrCountStringFormatter": "readonly",
-    "GrDiffBuilder": "readonly",
-    "GrDiffBuilderBinary": "readonly",
-    "GrDiffBuilderImage": "readonly",
-    "GrDiffBuilderSideBySide": "readonly",
-    "GrDiffBuilderUnified": "readonly",
-    "GrDiffGroup": "readonly",
-    "GrDiffLine": "readonly",
-    "GrDomHook": "readonly",
-    "GrDomHooksManager": "readonly",
-    "GrEditConstants": "readonly",
     "GrEmailSuggestionsProvider": "readonly",
-    "GrEtagDecorator": "readonly",
     "GrEventHelper": "readonly",
-    "GrFileListConstants": "readonly",
     "GrGroupSuggestionsProvider": "readonly",
-    "GrLinkTextParser": "readonly",
     "GrPluginActionContext": "readonly",
-    "GrPluginEndpoints": "readonly",
     "GrPluginRestApi": "readonly",
-    "GrPopupInterface": "readonly",
-    "GrRangeNormalizer": "readonly",
     "GrRepoApi": "readonly",
     "GrReporting": "readonly",
-    "GrRestApiHelper": "readonly",
-    "GrReviewerSuggestionsProvider": "readonly",
-    "GrReviewerUpdatesParser": "readonly",
     "GrSettingsApi": "readonly",
     "GrStylesApi": "readonly",
-    "GrThemeApi": "readonly",
     "PluginLoader": "readonly",
-    "SiteBasedCache": "readonly",
     "util": "readonly",
     // Global variables from 3rd party libraries.
     // You should not add anything in this list, always try to import
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 4f73d86..3878a26 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -37,7 +37,6 @@
 import '../gr-commit-info/gr-commit-info.js';
 import '../gr-reviewer-list/gr-reviewer-list.js';
 import '../../shared/gr-account-list/gr-account-list.js';
-import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -45,6 +44,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-metadata_html.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {GrReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 
 const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index dfb378e..7c09dd1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -21,14 +21,12 @@
 import '../../core/gr-navigation/gr-navigation.js';
 import '../../core/gr-reporting/gr-reporting.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
-import '../../edit/gr-edit-constants.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
 import '../../shared/gr-account-link/gr-account-link.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-change-star/gr-change-star.js';
 import '../../shared/gr-change-status/gr-change-status.js';
-import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
 import '../../shared/gr-editable-content/gr-editable-content.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
@@ -60,6 +58,8 @@
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {GrEditConstants} from '../../edit/gr-edit-constants.js';
+import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 
 import {PrimaryTabs, SecondaryTabs} from '../../../constants/constants.js';
 import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages.js';
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 1c82e5e..02d21a2e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -45,6 +45,9 @@
 
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {GrEditConstants} from '../../edit/gr-edit-constants.js';
+import {GrPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
+
 suite('gr-change-view tests', () => {
   const kb = KeyboardShortcutBinder;
   kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-constants.js b/polygerrit-ui/app/elements/change/gr-file-list-constants.js
index 0f93b52..5bba786 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-constants.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-constants.js
@@ -14,16 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  const GrFileListConstants = window.GrFileListConstants || {};
-
-  GrFileListConstants.FilesExpandedState = {
+export const GrFileListConstants = {
+  FilesExpandedState: {
     ALL: 'all',
     NONE: 'none',
     SOME: 'some',
-  };
+  },
+};
 
-  window.GrFileListConstants = GrFileListConstants;
-})(window);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index 78fc4b7..b1bacf3 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -27,7 +27,6 @@
 import '../../shared/gr-select/gr-select.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-icons/gr-icons.js';
-import '../gr-file-list-constants.js';
 import '../gr-commit-info/gr-commit-info.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -37,6 +36,7 @@
 import {htmlTemplate} from './gr-file-list-header_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {GrFileListConstants} from '../gr-file-list-constants.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index 745d25e..59a6384 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -41,6 +41,8 @@
 import '../../../test/common-test-setup.js';
 import './gr-file-list-header.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GrFileListConstants} from '../gr-file-list-constants.js';
+
 suite('gr-file-list-header tests', () => {
   let element;
   let sandbox;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 18e9c18..41b2a322 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -29,10 +29,8 @@
 import '../../shared/gr-linked-text/gr-linked-text.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-select/gr-select.js';
-import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
-import '../gr-file-list-constants.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -44,6 +42,8 @@
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {GrFileListConstants} from '../gr-file-list-constants.js';
+import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 9a94de7..ccff964 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -50,6 +50,8 @@
 import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {GrFileListConstants} from '../gr-file-list-constants.js';
+
 suite('gr-file-list tests', () => {
   const kb = KeyboardShortcutBinder;
   kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 90c2e4e..c3b2a9e 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -32,7 +32,6 @@
 import '../gr-thread-list/gr-thread-list.js';
 import '../../../styles/shared-styles.js';
 import '../gr-comment-list/gr-comment-list.js';
-import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -42,6 +41,7 @@
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {GrReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
index 1f96cec..a65fdca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
@@ -14,34 +14,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window, GrDiffBuilder) {
-  'use strict';
 
-  // Prevent redefinition.
-  if (window.GrDiffBuilderBinary) { return; }
+import {GrDiffBuilder} from './gr-diff-builder.js';
 
-  /** @constructor */
-  function GrDiffBuilderBinary(diff, prefs, outputEl) {
-    GrDiffBuilder.call(this, diff, prefs, outputEl);
-  }
+/** @constructor */
+export function GrDiffBuilderBinary(diff, prefs, outputEl) {
+  GrDiffBuilder.call(this, diff, prefs, outputEl);
+}
 
-  GrDiffBuilderBinary.prototype = Object.create(GrDiffBuilder.prototype);
-  GrDiffBuilderBinary.prototype.constructor = GrDiffBuilderBinary;
+GrDiffBuilderBinary.prototype = Object.create(GrDiffBuilder.prototype);
+GrDiffBuilderBinary.prototype.constructor = GrDiffBuilderBinary;
 
-  // This method definition is a no-op to satisfy the parent type.
-  GrDiffBuilderBinary.prototype.addColumns = function(outputEl, fontSize) {};
+// This method definition is a no-op to satisfy the parent type.
+GrDiffBuilderBinary.prototype.addColumns = function(outputEl, fontSize) {};
 
-  GrDiffBuilderBinary.prototype.buildSectionElement = function() {
-    const section = this._createElement('tbody', 'binary-diff');
-    const row = this._createElement('tr');
-    const cell = this._createElement('td');
-    const label = this._createElement('label');
-    label.textContent = 'Difference in binary files';
-    cell.appendChild(label);
-    row.appendChild(cell);
-    section.appendChild(row);
-    return section;
-  };
-
-  window.GrDiffBuilderBinary = GrDiffBuilderBinary;
-})(window, GrDiffBuilder);
+GrDiffBuilderBinary.prototype.buildSectionElement = function() {
+  const section = this._createElement('tbody', 'binary-diff');
+  const row = this._createElement('tr');
+  const cell = this._createElement('td');
+  const label = this._createElement('label');
+  label.textContent = 'Difference in binary files';
+  cell.appendChild(label);
+  row.appendChild(cell);
+  section.appendChild(row);
+  return section;
+};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
index ae93bdd..ae363e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -21,19 +21,18 @@
 import '../../shared/gr-hovercard/gr-hovercard.js';
 import '../gr-ranged-comment-layer/gr-ranged-comment-layer.js';
 import '../../../scripts/util.js';
-import '../gr-diff/gr-diff-line.js';
-import '../gr-diff/gr-diff-group.js';
-import './gr-diff-builder.js';
 import './gr-diff-builder-side-by-side.js';
-import './gr-diff-builder-unified.js';
-import './gr-diff-builder-image.js';
-import './gr-diff-builder-binary.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-builder-element_html.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
+import {GrDiffBuilder} from './gr-diff-builder.js';
+import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
+import {GrDiffBuilderImage} from './gr-diff-builder-image.js';
+import {GrDiffBuilderUnified} from './gr-diff-builder-unified.js';
+import {GrDiffBuilderBinary} from './gr-diff-builder-binary.js';
 
 const DiffViewMode = {
   SIDE_BY_SIDE: 'SIDE_BY_SIDE',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
index 556ea56..da4c2a4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
@@ -49,7 +49,6 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../../../scripts/util.js';
-import '../gr-diff/gr-diff-line.js';
 import '../gr-diff/gr-diff-group.js';
 import './gr-diff-builder.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -57,6 +56,10 @@
 import './gr-diff-builder-element.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
+import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
+import {GrDiffBuilder} from './gr-diff-builder.js';
+
 const DiffViewMode = {
   SIDE_BY_SIDE: 'SIDE_BY_SIDE',
   UNIFIED: 'UNIFIED_DIFF',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index a7a29db..1fc0d4f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -14,170 +14,165 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window, GrDiffBuilderSideBySide) {
-  'use strict';
 
-  // Prevent redefinition.
-  if (window.GrDiffBuilderImage) { return; }
+import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
 
-  // MIME types for images we allow showing. Do not include SVG, it can contain
-  // arbitrary JavaScript.
-  const IMAGE_MIME_PATTERN = /^image\/(bmp|gif|x-icon|jpeg|jpg|png|tiff|webp)$/;
+// MIME types for images we allow showing. Do not include SVG, it can contain
+// arbitrary JavaScript.
+const IMAGE_MIME_PATTERN = /^image\/(bmp|gif|x-icon|jpeg|jpg|png|tiff|webp)$/;
 
-  /** @constructor */
-  function GrDiffBuilderImage(diff, prefs, outputEl, baseImage, revisionImage) {
-    GrDiffBuilderSideBySide.call(this, diff, prefs, outputEl, []);
-    this._baseImage = baseImage;
-    this._revisionImage = revisionImage;
+/** @constructor */
+export function GrDiffBuilderImage(diff, prefs, outputEl, baseImage,
+    revisionImage) {
+  GrDiffBuilderSideBySide.call(this, diff, prefs, outputEl, []);
+  this._baseImage = baseImage;
+  this._revisionImage = revisionImage;
+}
+
+GrDiffBuilderImage.prototype = Object.create(
+    GrDiffBuilderSideBySide.prototype);
+GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
+
+GrDiffBuilderImage.prototype.renderDiff = function() {
+  const section = this._createElement('tbody', 'image-diff');
+
+  this._emitImagePair(section);
+  this._emitImageLabels(section);
+
+  this._outputEl.appendChild(section);
+  this._outputEl.appendChild(this._createEndpoint());
+};
+
+GrDiffBuilderImage.prototype._createEndpoint = function() {
+  const tbody = this._createElement('tbody');
+  const tr = this._createElement('tr');
+  const td = this._createElement('td');
+
+  // TODO(kaspern): Support blame for image diffs and remove the hardcoded 4
+  // column limit.
+  td.setAttribute('colspan', '4');
+  const endpoint = this._createElement('gr-endpoint-decorator');
+  const endpointDomApi = Polymer.dom(endpoint);
+  endpointDomApi.setAttribute('name', 'image-diff');
+  endpointDomApi.appendChild(
+      this._createEndpointParam('baseImage', this._baseImage));
+  endpointDomApi.appendChild(
+      this._createEndpointParam('revisionImage', this._revisionImage));
+  td.appendChild(endpoint);
+  tr.appendChild(td);
+  tbody.appendChild(tr);
+  return tbody;
+};
+
+GrDiffBuilderImage.prototype._createEndpointParam = function(name, value) {
+  const endpointParam = this._createElement('gr-endpoint-param');
+  endpointParam.setAttribute('name', name);
+  endpointParam.value = value;
+  return endpointParam;
+};
+
+GrDiffBuilderImage.prototype._emitImagePair = function(section) {
+  const tr = this._createElement('tr');
+
+  tr.appendChild(this._createElement('td', 'left lineNum blank'));
+  tr.appendChild(this._createImageCell(this._baseImage, 'left', section));
+
+  tr.appendChild(this._createElement('td', 'right lineNum blank'));
+  tr.appendChild(this._createImageCell(
+      this._revisionImage, 'right', section));
+
+  section.appendChild(tr);
+};
+
+GrDiffBuilderImage.prototype._createImageCell = function(image, className,
+    section) {
+  const td = this._createElement('td', className);
+  if (image && IMAGE_MIME_PATTERN.test(image.type)) {
+    const imageEl = this._createElement('img');
+    imageEl.onload = function() {
+      image._height = imageEl.naturalHeight;
+      image._width = imageEl.naturalWidth;
+      this._updateImageLabel(section, className, image);
+    }.bind(this);
+    imageEl.setAttribute('src', `data:${image.type};base64, ${image.body}`);
+    imageEl.addEventListener('error', () => {
+      imageEl.remove();
+      td.textContent = '[Image failed to load]';
+    });
+    td.appendChild(imageEl);
+  }
+  return td;
+};
+
+GrDiffBuilderImage.prototype._updateImageLabel = function(section, className,
+    image) {
+  const label = Polymer.dom(section)
+      .querySelector('.' + className + ' span.label');
+  this._setLabelText(label, image);
+};
+
+GrDiffBuilderImage.prototype._setLabelText = function(label, image) {
+  label.textContent = this._getImageLabel(image);
+};
+
+GrDiffBuilderImage.prototype._emitImageLabels = function(section) {
+  const tr = this._createElement('tr');
+
+  let addNamesInLabel = false;
+
+  if (this._baseImage && this._revisionImage &&
+      this._baseImage._name !== this._revisionImage._name) {
+    addNamesInLabel = true;
   }
 
-  GrDiffBuilderImage.prototype = Object.create(
-      GrDiffBuilderSideBySide.prototype);
-  GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
+  tr.appendChild(this._createElement('td', 'left lineNum blank'));
+  let td = this._createElement('td', 'left');
+  let label = this._createElement('label');
+  let nameSpan;
+  let labelSpan = this._createElement('span', 'label');
 
-  GrDiffBuilderImage.prototype.renderDiff = function() {
-    const section = this._createElement('tbody', 'image-diff');
+  if (addNamesInLabel) {
+    nameSpan = this._createElement('span', 'name');
+    nameSpan.textContent = this._baseImage._name;
+    label.appendChild(nameSpan);
+    label.appendChild(this._createElement('br'));
+  }
 
-    this._emitImagePair(section);
-    this._emitImageLabels(section);
+  this._setLabelText(labelSpan, this._baseImage, addNamesInLabel);
 
-    this._outputEl.appendChild(section);
-    this._outputEl.appendChild(this._createEndpoint());
-  };
+  label.appendChild(labelSpan);
+  td.appendChild(label);
+  tr.appendChild(td);
 
-  GrDiffBuilderImage.prototype._createEndpoint = function() {
-    const tbody = this._createElement('tbody');
-    const tr = this._createElement('tr');
-    const td = this._createElement('td');
+  tr.appendChild(this._createElement('td', 'right lineNum blank'));
+  td = this._createElement('td', 'right');
+  label = this._createElement('label');
+  labelSpan = this._createElement('span', 'label');
 
-    // TODO(kaspern): Support blame for image diffs and remove the hardcoded 4
-    // column limit.
-    td.setAttribute('colspan', '4');
-    const endpoint = this._createElement('gr-endpoint-decorator');
-    const endpointDomApi = Polymer.dom(endpoint);
-    endpointDomApi.setAttribute('name', 'image-diff');
-    endpointDomApi.appendChild(
-        this._createEndpointParam('baseImage', this._baseImage));
-    endpointDomApi.appendChild(
-        this._createEndpointParam('revisionImage', this._revisionImage));
-    td.appendChild(endpoint);
-    tr.appendChild(td);
-    tbody.appendChild(tr);
-    return tbody;
-  };
+  if (addNamesInLabel) {
+    nameSpan = this._createElement('span', 'name');
+    nameSpan.textContent = this._revisionImage._name;
+    label.appendChild(nameSpan);
+    label.appendChild(this._createElement('br'));
+  }
 
-  GrDiffBuilderImage.prototype._createEndpointParam = function(name, value) {
-    const endpointParam = this._createElement('gr-endpoint-param');
-    endpointParam.setAttribute('name', name);
-    endpointParam.value = value;
-    return endpointParam;
-  };
+  this._setLabelText(labelSpan, this._revisionImage, addNamesInLabel);
 
-  GrDiffBuilderImage.prototype._emitImagePair = function(section) {
-    const tr = this._createElement('tr');
+  label.appendChild(labelSpan);
+  td.appendChild(label);
+  tr.appendChild(td);
 
-    tr.appendChild(this._createElement('td', 'left lineNum blank'));
-    tr.appendChild(this._createImageCell(this._baseImage, 'left', section));
+  section.appendChild(tr);
+};
 
-    tr.appendChild(this._createElement('td', 'right lineNum blank'));
-    tr.appendChild(this._createImageCell(
-        this._revisionImage, 'right', section));
-
-    section.appendChild(tr);
-  };
-
-  GrDiffBuilderImage.prototype._createImageCell = function(image, className,
-      section) {
-    const td = this._createElement('td', className);
-    if (image && IMAGE_MIME_PATTERN.test(image.type)) {
-      const imageEl = this._createElement('img');
-      imageEl.onload = function() {
-        image._height = imageEl.naturalHeight;
-        image._width = imageEl.naturalWidth;
-        this._updateImageLabel(section, className, image);
-      }.bind(this);
-      imageEl.setAttribute('src', `data:${image.type};base64, ${image.body}`);
-      imageEl.addEventListener('error', () => {
-        imageEl.remove();
-        td.textContent = '[Image failed to load]';
-      });
-      td.appendChild(imageEl);
+GrDiffBuilderImage.prototype._getImageLabel = function(image) {
+  if (image) {
+    const type = image.type || image._expectedType;
+    if (image._width && image._height) {
+      return image._width + '×' + image._height + ' ' + type;
+    } else {
+      return type;
     }
-    return td;
-  };
-
-  GrDiffBuilderImage.prototype._updateImageLabel = function(section, className,
-      image) {
-    const label = Polymer.dom(section)
-        .querySelector('.' + className + ' span.label');
-    this._setLabelText(label, image);
-  };
-
-  GrDiffBuilderImage.prototype._setLabelText = function(label, image) {
-    label.textContent = this._getImageLabel(image);
-  };
-
-  GrDiffBuilderImage.prototype._emitImageLabels = function(section) {
-    const tr = this._createElement('tr');
-
-    let addNamesInLabel = false;
-
-    if (this._baseImage && this._revisionImage &&
-        this._baseImage._name !== this._revisionImage._name) {
-      addNamesInLabel = true;
-    }
-
-    tr.appendChild(this._createElement('td', 'left lineNum blank'));
-    let td = this._createElement('td', 'left');
-    let label = this._createElement('label');
-    let nameSpan;
-    let labelSpan = this._createElement('span', 'label');
-
-    if (addNamesInLabel) {
-      nameSpan = this._createElement('span', 'name');
-      nameSpan.textContent = this._baseImage._name;
-      label.appendChild(nameSpan);
-      label.appendChild(this._createElement('br'));
-    }
-
-    this._setLabelText(labelSpan, this._baseImage, addNamesInLabel);
-
-    label.appendChild(labelSpan);
-    td.appendChild(label);
-    tr.appendChild(td);
-
-    tr.appendChild(this._createElement('td', 'right lineNum blank'));
-    td = this._createElement('td', 'right');
-    label = this._createElement('label');
-    labelSpan = this._createElement('span', 'label');
-
-    if (addNamesInLabel) {
-      nameSpan = this._createElement('span', 'name');
-      nameSpan.textContent = this._revisionImage._name;
-      label.appendChild(nameSpan);
-      label.appendChild(this._createElement('br'));
-    }
-
-    this._setLabelText(labelSpan, this._revisionImage, addNamesInLabel);
-
-    label.appendChild(labelSpan);
-    td.appendChild(label);
-    tr.appendChild(td);
-
-    section.appendChild(tr);
-  };
-
-  GrDiffBuilderImage.prototype._getImageLabel = function(image) {
-    if (image) {
-      const type = image.type || image._expectedType;
-      if (image._width && image._height) {
-        return image._width + '×' + image._height + ' ' + type;
-      } else {
-        return type;
-      }
-    }
-    return 'No image';
-  };
-
-  window.GrDiffBuilderImage = GrDiffBuilderImage;
-})(window, GrDiffBuilderSideBySide);
+  }
+  return 'No image';
+};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
index 5fe9f3a..8b73936 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -14,106 +14,100 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window, GrDiffBuilder) {
-  'use strict';
 
-  // Prevent redefinition.
-  if (window.GrDiffBuilderSideBySide) { return; }
+import {GrDiffBuilder} from './gr-diff-builder.js';
 
-  /** @constructor */
-  function GrDiffBuilderSideBySide(diff, prefs, outputEl, layers) {
-    GrDiffBuilder.call(this, diff, prefs, outputEl, layers);
+/** @constructor */
+export function GrDiffBuilderSideBySide(diff, prefs, outputEl, layers) {
+  GrDiffBuilder.call(this, diff, prefs, outputEl, layers);
+}
+GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
+GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
+
+GrDiffBuilderSideBySide.prototype.buildSectionElement = function(group) {
+  const sectionEl = this._createElement('tbody', 'section');
+  sectionEl.classList.add(group.type);
+  if (this._isTotal(group)) {
+    sectionEl.classList.add('total');
   }
-  GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
-  GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
+  if (group.dueToRebase) {
+    sectionEl.classList.add('dueToRebase');
+  }
+  if (group.ignoredWhitespaceOnly) {
+    sectionEl.classList.add('ignoredWhitespaceOnly');
+  }
+  const pairs = group.getSideBySidePairs();
+  for (let i = 0; i < pairs.length; i++) {
+    sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left,
+        pairs[i].right));
+  }
+  return sectionEl;
+};
 
-  GrDiffBuilderSideBySide.prototype.buildSectionElement = function(group) {
-    const sectionEl = this._createElement('tbody', 'section');
-    sectionEl.classList.add(group.type);
-    if (this._isTotal(group)) {
-      sectionEl.classList.add('total');
-    }
-    if (group.dueToRebase) {
-      sectionEl.classList.add('dueToRebase');
-    }
-    if (group.ignoredWhitespaceOnly) {
-      sectionEl.classList.add('ignoredWhitespaceOnly');
-    }
-    const pairs = group.getSideBySidePairs();
-    for (let i = 0; i < pairs.length; i++) {
-      sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left,
-          pairs[i].right));
-    }
-    return sectionEl;
-  };
+GrDiffBuilderSideBySide.prototype.addColumns = function(outputEl, fontSize) {
+  const width = fontSize * 4;
+  const colgroup = document.createElement('colgroup');
 
-  GrDiffBuilderSideBySide.prototype.addColumns = function(outputEl, fontSize) {
-    const width = fontSize * 4;
-    const colgroup = document.createElement('colgroup');
+  // Add the blame column.
+  let col = this._createElement('col', 'blame');
+  colgroup.appendChild(col);
 
-    // Add the blame column.
-    let col = this._createElement('col', 'blame');
-    colgroup.appendChild(col);
+  // Add left-side line number.
+  col = document.createElement('col');
+  col.setAttribute('width', width);
+  colgroup.appendChild(col);
 
-    // Add left-side line number.
-    col = document.createElement('col');
-    col.setAttribute('width', width);
-    colgroup.appendChild(col);
+  // Add left-side content.
+  colgroup.appendChild(document.createElement('col'));
 
-    // Add left-side content.
-    colgroup.appendChild(document.createElement('col'));
+  // Add right-side line number.
+  col = document.createElement('col');
+  col.setAttribute('width', width);
+  colgroup.appendChild(col);
 
-    // Add right-side line number.
-    col = document.createElement('col');
-    col.setAttribute('width', width);
-    colgroup.appendChild(col);
+  // Add right-side content.
+  colgroup.appendChild(document.createElement('col'));
 
-    // Add right-side content.
-    colgroup.appendChild(document.createElement('col'));
+  outputEl.appendChild(colgroup);
+};
 
-    outputEl.appendChild(colgroup);
-  };
+GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine,
+    rightLine) {
+  const row = this._createElement('tr');
+  row.classList.add('diff-row', 'side-by-side');
+  row.setAttribute('left-type', leftLine.type);
+  row.setAttribute('right-type', rightLine.type);
+  row.tabIndex = -1;
 
-  GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine,
-      rightLine) {
-    const row = this._createElement('tr');
-    row.classList.add('diff-row', 'side-by-side');
-    row.setAttribute('left-type', leftLine.type);
-    row.setAttribute('right-type', rightLine.type);
-    row.tabIndex = -1;
+  row.appendChild(this._createBlameCell(leftLine));
 
-    row.appendChild(this._createBlameCell(leftLine));
+  this._appendPair(section, row, leftLine, leftLine.beforeNumber,
+      GrDiffBuilder.Side.LEFT);
+  this._appendPair(section, row, rightLine, rightLine.afterNumber,
+      GrDiffBuilder.Side.RIGHT);
+  return row;
+};
 
-    this._appendPair(section, row, leftLine, leftLine.beforeNumber,
-        GrDiffBuilder.Side.LEFT);
-    this._appendPair(section, row, rightLine, rightLine.afterNumber,
-        GrDiffBuilder.Side.RIGHT);
-    return row;
-  };
+GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line,
+    lineNumber, side) {
+  const lineNumberEl = this._createLineEl(line, lineNumber, line.type, side);
+  row.appendChild(lineNumberEl);
+  const action = this._createContextControl(section, line);
+  if (action) {
+    row.appendChild(action);
+  } else {
+    const textEl = this._createTextEl(lineNumberEl, line, side);
+    row.appendChild(textEl);
+  }
+};
 
-  GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line,
-      lineNumber, side) {
-    const lineNumberEl = this._createLineEl(line, lineNumber, line.type, side);
-    row.appendChild(lineNumberEl);
-    const action = this._createContextControl(section, line);
-    if (action) {
-      row.appendChild(action);
-    } else {
-      const textEl = this._createTextEl(lineNumberEl, line, side);
-      row.appendChild(textEl);
-    }
-  };
-
-  GrDiffBuilderSideBySide.prototype._getNextContentOnSide = function(
-      content, side) {
-    let tr = content.parentElement.parentElement;
-    while (tr = tr.nextSibling) {
-      content = tr.querySelector(
-          'td.content .contentText[data-side="' + side + '"]');
-      if (content) { return content; }
-    }
-    return null;
-  };
-
-  window.GrDiffBuilderSideBySide = GrDiffBuilderSideBySide;
-})(window, GrDiffBuilder);
+GrDiffBuilderSideBySide.prototype._getNextContentOnSide = function(
+    content, side) {
+  let tr = content.parentElement.parentElement;
+  while (tr = tr.nextSibling) {
+    content = tr.querySelector(
+        'td.content .contentText[data-side="' + side + '"]');
+    if (content) { return content; }
+  }
+  return null;
+};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
index a8c9bf6..8163176 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -14,102 +14,96 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window, GrDiffBuilder) {
-  'use strict';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
+import {GrDiffBuilder} from './gr-diff-builder.js';
 
-  // Prevent redefinition.
-  if (window.GrDiffBuilderUnified) { return; }
+export function GrDiffBuilderUnified(diff, prefs, outputEl, layers) {
+  GrDiffBuilder.call(this, diff, prefs, outputEl, layers);
+}
+GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
+GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
 
-  function GrDiffBuilderUnified(diff, prefs, outputEl, layers) {
-    GrDiffBuilder.call(this, diff, prefs, outputEl, layers);
+GrDiffBuilderUnified.prototype.buildSectionElement = function(group) {
+  const sectionEl = this._createElement('tbody', 'section');
+  sectionEl.classList.add(group.type);
+  if (this._isTotal(group)) {
+    sectionEl.classList.add('total');
   }
-  GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
-  GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
+  if (group.dueToRebase) {
+    sectionEl.classList.add('dueToRebase');
+  }
+  if (group.ignoredWhitespaceOnly) {
+    sectionEl.classList.add('ignoredWhitespaceOnly');
+  }
 
-  GrDiffBuilderUnified.prototype.buildSectionElement = function(group) {
-    const sectionEl = this._createElement('tbody', 'section');
-    sectionEl.classList.add(group.type);
-    if (this._isTotal(group)) {
-      sectionEl.classList.add('total');
+  for (let i = 0; i < group.lines.length; ++i) {
+    const line = group.lines[i];
+    // If only whitespace has changed and the settings ask for whitespace to
+    // be ignored, only render the right-side line in unified diff mode.
+    if (group.ignoredWhitespaceOnly && line.type == GrDiffLine.Type.REMOVE) {
+      continue;
     }
-    if (group.dueToRebase) {
-      sectionEl.classList.add('dueToRebase');
+    sectionEl.appendChild(this._createRow(sectionEl, line));
+  }
+  return sectionEl;
+};
+
+GrDiffBuilderUnified.prototype.addColumns = function(outputEl, fontSize) {
+  const width = fontSize * 4;
+  const colgroup = document.createElement('colgroup');
+
+  // Add the blame column.
+  let col = this._createElement('col', 'blame');
+  colgroup.appendChild(col);
+
+  // Add left-side line number.
+  col = document.createElement('col');
+  col.setAttribute('width', width);
+  colgroup.appendChild(col);
+
+  // Add right-side line number.
+  col = document.createElement('col');
+  col.setAttribute('width', width);
+  colgroup.appendChild(col);
+
+  // Add the content.
+  colgroup.appendChild(document.createElement('col'));
+
+  outputEl.appendChild(colgroup);
+};
+
+GrDiffBuilderUnified.prototype._createRow = function(section, line) {
+  const row = this._createElement('tr', line.type);
+  row.classList.add('diff-row', 'unified');
+  row.tabIndex = -1;
+  row.appendChild(this._createBlameCell(line));
+
+  let lineNumberEl = this._createLineEl(line, line.beforeNumber,
+      GrDiffLine.Type.REMOVE, 'left');
+  row.appendChild(lineNumberEl);
+  lineNumberEl = this._createLineEl(line, line.afterNumber,
+      GrDiffLine.Type.ADD, 'right');
+  row.appendChild(lineNumberEl);
+
+  const action = this._createContextControl(section, line);
+  if (action) {
+    row.appendChild(action);
+  } else {
+    const textEl = this._createTextEl(lineNumberEl, line);
+    row.appendChild(textEl);
+  }
+  return row;
+};
+
+GrDiffBuilderUnified.prototype._getNextContentOnSide = function(
+    content, side) {
+  let tr = content.parentElement.parentElement;
+  while (tr = tr.nextSibling) {
+    if (tr.classList.contains('both') || (
+      (side === 'left' && tr.classList.contains('remove')) ||
+        (side === 'right' && tr.classList.contains('add')))) {
+      return tr.querySelector('.contentText');
     }
-    if (group.ignoredWhitespaceOnly) {
-      sectionEl.classList.add('ignoredWhitespaceOnly');
-    }
-
-    for (let i = 0; i < group.lines.length; ++i) {
-      const line = group.lines[i];
-      // If only whitespace has changed and the settings ask for whitespace to
-      // be ignored, only render the right-side line in unified diff mode.
-      if (group.ignoredWhitespaceOnly && line.type == GrDiffLine.Type.REMOVE) {
-        continue;
-      }
-      sectionEl.appendChild(this._createRow(sectionEl, line));
-    }
-    return sectionEl;
-  };
-
-  GrDiffBuilderUnified.prototype.addColumns = function(outputEl, fontSize) {
-    const width = fontSize * 4;
-    const colgroup = document.createElement('colgroup');
-
-    // Add the blame column.
-    let col = this._createElement('col', 'blame');
-    colgroup.appendChild(col);
-
-    // Add left-side line number.
-    col = document.createElement('col');
-    col.setAttribute('width', width);
-    colgroup.appendChild(col);
-
-    // Add right-side line number.
-    col = document.createElement('col');
-    col.setAttribute('width', width);
-    colgroup.appendChild(col);
-
-    // Add the content.
-    colgroup.appendChild(document.createElement('col'));
-
-    outputEl.appendChild(colgroup);
-  };
-
-  GrDiffBuilderUnified.prototype._createRow = function(section, line) {
-    const row = this._createElement('tr', line.type);
-    row.classList.add('diff-row', 'unified');
-    row.tabIndex = -1;
-    row.appendChild(this._createBlameCell(line));
-
-    let lineNumberEl = this._createLineEl(line, line.beforeNumber,
-        GrDiffLine.Type.REMOVE, 'left');
-    row.appendChild(lineNumberEl);
-    lineNumberEl = this._createLineEl(line, line.afterNumber,
-        GrDiffLine.Type.ADD, 'right');
-    row.appendChild(lineNumberEl);
-
-    const action = this._createContextControl(section, line);
-    if (action) {
-      row.appendChild(action);
-    } else {
-      const textEl = this._createTextEl(lineNumberEl, line);
-      row.appendChild(textEl);
-    }
-    return row;
-  };
-
-  GrDiffBuilderUnified.prototype._getNextContentOnSide = function(
-      content, side) {
-    let tr = content.parentElement.parentElement;
-    while (tr = tr.nextSibling) {
-      if (tr.classList.contains('both') || (
-        (side === 'left' && tr.classList.contains('remove')) ||
-          (side === 'right' && tr.classList.contains('add')))) {
-        return tr.querySelector('.contentText');
-      }
-    }
-    return null;
-  };
-
-  window.GrDiffBuilderUnified = GrDiffBuilderUnified;
-})(window, GrDiffBuilder);
+  }
+  return null;
+};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
index 59838b4..c4fe353 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
@@ -27,10 +27,13 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../../../scripts/util.js';
-import '../gr-diff/gr-diff-line.js';
 import '../gr-diff/gr-diff-group.js';
 import './gr-diff-builder.js';
 import './gr-diff-builder-unified.js';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
+import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
+import {GrDiffBuilderUnified} from './gr-diff-builder-unified.js';
+
 suite('GrDiffBuilderUnified tests', () => {
   let prefs;
   let outputEl;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 5aa2d14..a9482c9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -14,612 +14,606 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window, GrDiffGroup, GrDiffLine) {
-  'use strict';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
+import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
 
-  // Prevent redefinition.
-  if (window.GrDiffBuilder) { return; }
+/**
+ * In JS, unicode code points above 0xFFFF occupy two elements of a string.
+ * For example '𐀏'.length is 2. An occurence of such a code point is called a
+ * surrogate pair.
+ *
+ * This regex segments a string along tabs ('\t') and surrogate pairs, since
+ * these are two cases where '1 char' does not automatically imply '1 column'.
+ *
+ * TODO: For human languages whose orthographies use combining marks, this
+ * approach won't correctly identify the grapheme boundaries. In those cases,
+ * a grapheme consists of multiple code points that should count as only one
+ * character against the column limit. Getting that correct (if it's desired)
+ * is probably beyond the limits of a regex, but there are nonstandard APIs to
+ * do this, and proposed (but, as of Nov 2017, unimplemented) standard APIs.
+ *
+ * Further reading:
+ *   On Unicode in JS: https://mathiasbynens.be/notes/javascript-unicode
+ *   Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
+ *   A proposed JS API: https://github.com/tc39/proposal-intl-segmenter
+ */
+const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
 
-  /**
-   * In JS, unicode code points above 0xFFFF occupy two elements of a string.
-   * For example '𐀏'.length is 2. An occurence of such a code point is called a
-   * surrogate pair.
-   *
-   * This regex segments a string along tabs ('\t') and surrogate pairs, since
-   * these are two cases where '1 char' does not automatically imply '1 column'.
-   *
-   * TODO: For human languages whose orthographies use combining marks, this
-   * approach won't correctly identify the grapheme boundaries. In those cases,
-   * a grapheme consists of multiple code points that should count as only one
-   * character against the column limit. Getting that correct (if it's desired)
-   * is probably beyond the limits of a regex, but there are nonstandard APIs to
-   * do this, and proposed (but, as of Nov 2017, unimplemented) standard APIs.
-   *
-   * Further reading:
-   *   On Unicode in JS: https://mathiasbynens.be/notes/javascript-unicode
-   *   Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
-   *   A proposed JS API: https://github.com/tc39/proposal-intl-segmenter
-   */
-  const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+export function GrDiffBuilder(diff, prefs, outputEl, layers) {
+  this._diff = diff;
+  this._prefs = prefs;
+  this._outputEl = outputEl;
+  this.groups = [];
+  this._blameInfo = null;
 
-  function GrDiffBuilder(diff, prefs, outputEl, layers) {
-    this._diff = diff;
-    this._prefs = prefs;
-    this._outputEl = outputEl;
-    this.groups = [];
-    this._blameInfo = null;
+  this.layers = layers || [];
 
-    this.layers = layers || [];
+  if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
+    throw Error('Invalid tab size from preferences.');
+  }
 
-    if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
-      throw Error('Invalid tab size from preferences.');
+  if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
+    throw Error('Invalid line length from preferences.');
+  }
+
+  for (const layer of this.layers) {
+    if (layer.addListener) {
+      layer.addListener(this._handleLayerUpdate.bind(this));
+    }
+  }
+}
+
+GrDiffBuilder.GroupType = {
+  ADDED: 'b',
+  BOTH: 'ab',
+  REMOVED: 'a',
+};
+
+GrDiffBuilder.Highlights = {
+  ADDED: 'edit_b',
+  REMOVED: 'edit_a',
+};
+
+GrDiffBuilder.Side = {
+  LEFT: 'left',
+  RIGHT: 'right',
+};
+
+GrDiffBuilder.ContextButtonType = {
+  ABOVE: 'above',
+  BELOW: 'below',
+  ALL: 'all',
+};
+
+const PARTIAL_CONTEXT_AMOUNT = 10;
+
+/**
+ * Abstract method
+ *
+ * @param {string} outputEl
+ * @param {number} fontSize
+ */
+GrDiffBuilder.prototype.addColumns = function() {
+  throw Error('Subclasses must implement addColumns');
+};
+
+/**
+ * Abstract method
+ *
+ * @param {Object} group
+ */
+GrDiffBuilder.prototype.buildSectionElement = function() {
+  throw Error('Subclasses must implement buildSectionElement');
+};
+
+GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
+  const element = this.buildSectionElement(group);
+  this._outputEl.insertBefore(element, opt_beforeSection);
+  group.element = element;
+};
+
+GrDiffBuilder.prototype.getGroupsByLineRange = function(
+    startLine, endLine, opt_side) {
+  const groups = [];
+  for (let i = 0; i < this.groups.length; i++) {
+    const group = this.groups[i];
+    if (group.lines.length === 0) {
+      continue;
+    }
+    let groupStartLine = 0;
+    let groupEndLine = 0;
+    if (opt_side) {
+      groupStartLine = group.lineRange[opt_side].start;
+      groupEndLine = group.lineRange[opt_side].end;
     }
 
-    if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
-      throw Error('Invalid line length from preferences.');
+    if (groupStartLine === 0) { // Line was removed or added.
+      groupStartLine = groupEndLine;
     }
+    if (groupEndLine === 0) { // Line was removed or added.
+      groupEndLine = groupStartLine;
+    }
+    if (startLine <= groupEndLine && endLine >= groupStartLine) {
+      groups.push(group);
+    }
+  }
+  return groups;
+};
 
-    for (const layer of this.layers) {
-      if (layer.addListener) {
-        layer.addListener(this._handleLayerUpdate.bind(this));
+GrDiffBuilder.prototype.getContentByLine = function(lineNumber, opt_side,
+    opt_root) {
+  const root = Polymer.dom(opt_root || this._outputEl);
+  const sideSelector = opt_side ? ('.' + opt_side) : '';
+  return root.querySelector('td.lineNum[data-value="' + lineNumber +
+      '"]' + sideSelector + ' ~ td.content .contentText');
+};
+
+/**
+ * Find line elements or line objects by a range of line numbers and a side.
+ *
+ * @param {number} start The first line number
+ * @param {number} end The last line number
+ * @param {string} opt_side The side of the range. Either 'left' or 'right'.
+ * @param {!Array<GrDiffLine>} out_lines The output list of line objects. Use
+ *     null if not desired.
+ * @param  {!Array<HTMLElement>} out_elements The output list of line elements.
+ *     Use null if not desired.
+ */
+GrDiffBuilder.prototype.findLinesByRange = function(start, end, opt_side,
+    out_lines, out_elements) {
+  const groups = this.getGroupsByLineRange(start, end, opt_side);
+  for (const group of groups) {
+    let content = null;
+    for (const line of group.lines) {
+      if ((opt_side === 'left' && line.type === GrDiffLine.Type.ADD) ||
+          (opt_side === 'right' && line.type === GrDiffLine.Type.REMOVE)) {
+        continue;
+      }
+      const lineNumber = opt_side === 'left' ?
+        line.beforeNumber : line.afterNumber;
+      if (lineNumber < start || lineNumber > end) { continue; }
+
+      if (out_lines) { out_lines.push(line); }
+      if (out_elements) {
+        if (content) {
+          content = this._getNextContentOnSide(content, opt_side);
+        } else {
+          content = this.getContentByLine(lineNumber, opt_side,
+              group.element);
+        }
+        if (content) { out_elements.push(content); }
       }
     }
   }
+};
 
-  GrDiffBuilder.GroupType = {
-    ADDED: 'b',
-    BOTH: 'ab',
-    REMOVED: 'a',
-  };
-
-  GrDiffBuilder.Highlights = {
-    ADDED: 'edit_b',
-    REMOVED: 'edit_a',
-  };
-
-  GrDiffBuilder.Side = {
-    LEFT: 'left',
-    RIGHT: 'right',
-  };
-
-  GrDiffBuilder.ContextButtonType = {
-    ABOVE: 'above',
-    BELOW: 'below',
-    ALL: 'all',
-  };
-
-  const PARTIAL_CONTEXT_AMOUNT = 10;
-
-  /**
-   * Abstract method
-   *
-   * @param {string} outputEl
-   * @param {number} fontSize
-   */
-  GrDiffBuilder.prototype.addColumns = function() {
-    throw Error('Subclasses must implement addColumns');
-  };
-
-  /**
-   * Abstract method
-   *
-   * @param {Object} group
-   */
-  GrDiffBuilder.prototype.buildSectionElement = function() {
-    throw Error('Subclasses must implement buildSectionElement');
-  };
-
-  GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
-    const element = this.buildSectionElement(group);
-    this._outputEl.insertBefore(element, opt_beforeSection);
-    group.element = element;
-  };
-
-  GrDiffBuilder.prototype.getGroupsByLineRange = function(
-      startLine, endLine, opt_side) {
-    const groups = [];
-    for (let i = 0; i < this.groups.length; i++) {
-      const group = this.groups[i];
-      if (group.lines.length === 0) {
-        continue;
-      }
-      let groupStartLine = 0;
-      let groupEndLine = 0;
-      if (opt_side) {
-        groupStartLine = group.lineRange[opt_side].start;
-        groupEndLine = group.lineRange[opt_side].end;
-      }
-
-      if (groupStartLine === 0) { // Line was removed or added.
-        groupStartLine = groupEndLine;
-      }
-      if (groupEndLine === 0) { // Line was removed or added.
-        groupEndLine = groupStartLine;
-      }
-      if (startLine <= groupEndLine && endLine >= groupStartLine) {
-        groups.push(group);
-      }
+/**
+ * Re-renders the DIV.contentText elements for the given side and range of
+ * diff content.
+ */
+GrDiffBuilder.prototype._renderContentByRange = function(start, end, side) {
+  const lines = [];
+  const elements = [];
+  let line;
+  let el;
+  this.findLinesByRange(start, end, side, lines, elements);
+  for (let i = 0; i < lines.length; i++) {
+    line = lines[i];
+    el = elements[i];
+    if (!el) {
+      // Cannot re-render an element if it does not exist. This can happen
+      // if lines are collapsed and not visible on the page yet.
+      continue;
     }
-    return groups;
-  };
+    const lineNumberEl = this._getLineNumberEl(el, side);
+    el.parentElement.replaceChild(
+        this._createTextEl(lineNumberEl, line, side).firstChild,
+        el);
+  }
+};
 
-  GrDiffBuilder.prototype.getContentByLine = function(lineNumber, opt_side,
-      opt_root) {
-    const root = Polymer.dom(opt_root || this._outputEl);
-    const sideSelector = opt_side ? ('.' + opt_side) : '';
-    return root.querySelector('td.lineNum[data-value="' + lineNumber +
-        '"]' + sideSelector + ' ~ td.content .contentText');
-  };
+GrDiffBuilder.prototype.getSectionsByLineRange = function(
+    startLine, endLine, opt_side) {
+  return this.getGroupsByLineRange(startLine, endLine, opt_side).map(
+      group => group.element);
+};
 
-  /**
-   * Find line elements or line objects by a range of line numbers and a side.
-   *
-   * @param {number} start The first line number
-   * @param {number} end The last line number
-   * @param {string} opt_side The side of the range. Either 'left' or 'right'.
-   * @param {!Array<GrDiffLine>} out_lines The output list of line objects. Use
-   *     null if not desired.
-   * @param  {!Array<HTMLElement>} out_elements The output list of line elements.
-   *     Use null if not desired.
-   */
-  GrDiffBuilder.prototype.findLinesByRange = function(start, end, opt_side,
-      out_lines, out_elements) {
-    const groups = this.getGroupsByLineRange(start, end, opt_side);
-    for (const group of groups) {
-      let content = null;
-      for (const line of group.lines) {
-        if ((opt_side === 'left' && line.type === GrDiffLine.Type.ADD) ||
-            (opt_side === 'right' && line.type === GrDiffLine.Type.REMOVE)) {
-          continue;
-        }
-        const lineNumber = opt_side === 'left' ?
-          line.beforeNumber : line.afterNumber;
-        if (lineNumber < start || lineNumber > end) { continue; }
+GrDiffBuilder.prototype._createContextControl = function(section, line) {
+  if (!line.contextGroups) return null;
 
-        if (out_lines) { out_lines.push(line); }
-        if (out_elements) {
-          if (content) {
-            content = this._getNextContentOnSide(content, opt_side);
-          } else {
-            content = this.getContentByLine(lineNumber, opt_side,
-                group.element);
-          }
-          if (content) { out_elements.push(content); }
-        }
-      }
-    }
-  };
+  const numLines =
+      line.contextGroups[line.contextGroups.length - 1].lineRange.left.end -
+      line.contextGroups[0].lineRange.left.start + 1;
 
-  /**
-   * Re-renders the DIV.contentText elements for the given side and range of
-   * diff content.
-   */
-  GrDiffBuilder.prototype._renderContentByRange = function(start, end, side) {
-    const lines = [];
-    const elements = [];
-    let line;
-    let el;
-    this.findLinesByRange(start, end, side, lines, elements);
-    for (let i = 0; i < lines.length; i++) {
-      line = lines[i];
-      el = elements[i];
-      if (!el) {
-        // Cannot re-render an element if it does not exist. This can happen
-        // if lines are collapsed and not visible on the page yet.
-        continue;
-      }
-      const lineNumberEl = this._getLineNumberEl(el, side);
-      el.parentElement.replaceChild(
-          this._createTextEl(lineNumberEl, line, side).firstChild,
-          el);
-    }
-  };
+  if (numLines === 0) return null;
 
-  GrDiffBuilder.prototype.getSectionsByLineRange = function(
-      startLine, endLine, opt_side) {
-    return this.getGroupsByLineRange(startLine, endLine, opt_side).map(
-        group => group.element);
-  };
+  const td = this._createElement('td');
+  const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
 
-  GrDiffBuilder.prototype._createContextControl = function(section, line) {
-    if (!line.contextGroups) return null;
-
-    const numLines =
-        line.contextGroups[line.contextGroups.length - 1].lineRange.left.end -
-        line.contextGroups[0].lineRange.left.start + 1;
-
-    if (numLines === 0) return null;
-
-    const td = this._createElement('td');
-    const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
-
-    if (showPartialLinks) {
-      td.appendChild(this._createContextButton(
-          GrDiffBuilder.ContextButtonType.ABOVE, section, line, numLines));
-    }
-
+  if (showPartialLinks) {
     td.appendChild(this._createContextButton(
-        GrDiffBuilder.ContextButtonType.ALL, section, line, numLines));
+        GrDiffBuilder.ContextButtonType.ABOVE, section, line, numLines));
+  }
 
-    if (showPartialLinks) {
-      td.appendChild(this._createContextButton(
-          GrDiffBuilder.ContextButtonType.BELOW, section, line, numLines));
+  td.appendChild(this._createContextButton(
+      GrDiffBuilder.ContextButtonType.ALL, section, line, numLines));
+
+  if (showPartialLinks) {
+    td.appendChild(this._createContextButton(
+        GrDiffBuilder.ContextButtonType.BELOW, section, line, numLines));
+  }
+
+  return td;
+};
+
+GrDiffBuilder.prototype._createContextButton = function(type, section, line,
+    numLines) {
+  const context = PARTIAL_CONTEXT_AMOUNT;
+
+  const button = this._createElement('gr-button', 'showContext');
+  button.setAttribute('link', true);
+  button.setAttribute('no-uppercase', true);
+
+  let text;
+  let groups = []; // The groups that replace this one if tapped.
+  if (type === GrDiffBuilder.ContextButtonType.ALL) {
+    const icon = this._createElement('iron-icon', 'showContext');
+    icon.setAttribute('icon', 'gr-icons:unfold-more');
+    Polymer.dom(button).appendChild(icon);
+
+    text = 'Show ' + numLines + ' common line';
+    if (numLines > 1) { text += 's'; }
+    groups.push(...line.contextGroups);
+  } else if (type === GrDiffBuilder.ContextButtonType.ABOVE) {
+    text = '+' + context + ' above';
+    groups = GrDiffGroup.hideInContextControl(line.contextGroups,
+        context, numLines);
+  } else if (type === GrDiffBuilder.ContextButtonType.BELOW) {
+    text = '+' + context + ' below';
+    groups = GrDiffGroup.hideInContextControl(line.contextGroups,
+        0, numLines - context);
+  }
+  const textSpan = this._createElement('span', 'showContext');
+  Polymer.dom(textSpan).textContent = text;
+  Polymer.dom(button).appendChild(textSpan);
+
+  button.addEventListener('tap', e => {
+    e.detail = {
+      groups,
+      section,
+      numLines,
+    };
+    // Let it bubble up the DOM tree.
+  });
+
+  return button;
+};
+
+GrDiffBuilder.prototype._createLineEl = function(
+    line, number, type, side) {
+  const td = this._createElement('td');
+  if (side) {
+    td.classList.add(side);
+  }
+
+  // Add aria-labels for valid line numbers.
+  // For unified diff, this method will be called with number set to 0 for
+  // the empty line number column for added/removed lines. This should not
+  // be announced to the screenreader.
+  if (number > 0) {
+    if (line.type === GrDiffLine.Type.REMOVE) {
+      td.setAttribute('aria-label', `${number} removed`);
+    } else if (line.type === GrDiffLine.Type.ADD) {
+      td.setAttribute('aria-label', `${number} added`);
     }
+  }
 
+  if (line.type === GrDiffLine.Type.BLANK) {
     return td;
-  };
+  } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
+    td.classList.add('contextLineNum');
+  } else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
+    td.classList.add('lineNum');
+    td.setAttribute('data-value', number);
+    td.textContent = number === 'FILE' ? 'File' : number;
+  }
+  return td;
+};
 
-  GrDiffBuilder.prototype._createContextButton = function(type, section, line,
-      numLines) {
-    const context = PARTIAL_CONTEXT_AMOUNT;
+GrDiffBuilder.prototype._createTextEl = function(
+    lineNumberEl, line, opt_side) {
+  const td = this._createElement('td');
+  if (line.type !== GrDiffLine.Type.BLANK) {
+    td.classList.add('content');
+  }
 
-    const button = this._createElement('gr-button', 'showContext');
-    button.setAttribute('link', true);
-    button.setAttribute('no-uppercase', true);
+  // If intraline info is not available, the entire line will be
+  // considered as changed and marked as dark red / green color
+  if (!line.hasIntralineInfo) {
+    td.classList.add('no-intraline-info');
+  }
+  td.classList.add(line.type);
 
-    let text;
-    let groups = []; // The groups that replace this one if tapped.
-    if (type === GrDiffBuilder.ContextButtonType.ALL) {
-      const icon = this._createElement('iron-icon', 'showContext');
-      icon.setAttribute('icon', 'gr-icons:unfold-more');
-      Polymer.dom(button).appendChild(icon);
+  const lineLimit =
+      !this._prefs.line_wrapping ? this._prefs.line_length : Infinity;
 
-      text = 'Show ' + numLines + ' common line';
-      if (numLines > 1) { text += 's'; }
-      groups.push(...line.contextGroups);
-    } else if (type === GrDiffBuilder.ContextButtonType.ABOVE) {
-      text = '+' + context + ' above';
-      groups = GrDiffGroup.hideInContextControl(line.contextGroups,
-          context, numLines);
-    } else if (type === GrDiffBuilder.ContextButtonType.BELOW) {
-      text = '+' + context + ' below';
-      groups = GrDiffGroup.hideInContextControl(line.contextGroups,
-          0, numLines - context);
+  const contentText =
+      this._formatText(line.text, this._prefs.tab_size, lineLimit);
+  if (opt_side) {
+    contentText.setAttribute('data-side', opt_side);
+  }
+
+  for (const layer of this.layers) {
+    if (typeof layer.annotate == 'function') {
+      layer.annotate(contentText, lineNumberEl, line);
     }
-    const textSpan = this._createElement('span', 'showContext');
-    Polymer.dom(textSpan).textContent = text;
-    Polymer.dom(button).appendChild(textSpan);
+  }
 
-    button.addEventListener('tap', e => {
-      e.detail = {
-        groups,
-        section,
-        numLines,
-      };
-      // Let it bubble up the DOM tree.
-    });
+  td.appendChild(contentText);
 
-    return button;
-  };
+  return td;
+};
 
-  GrDiffBuilder.prototype._createLineEl = function(
-      line, number, type, side) {
-    const td = this._createElement('td');
-    if (side) {
-      td.classList.add(side);
-    }
+/**
+ * Returns a 'div' element containing the supplied |text| as its innerText,
+ * with '\t' characters expanded to a width determined by |tabSize|, and the
+ * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
+ * desired.
+ *
+ * @param {string} text The text to be formatted.
+ * @param {number} tabSize The width of each tab stop.
+ * @param {number} lineLimit The column after which to wrap lines.
+ * @return {HTMLElement}
+ */
+GrDiffBuilder.prototype._formatText = function(text, tabSize, lineLimit) {
+  const contentText = this._createElement('div', 'contentText');
 
-    // Add aria-labels for valid line numbers.
-    // For unified diff, this method will be called with number set to 0 for
-    // the empty line number column for added/removed lines. This should not
-    // be announced to the screenreader.
-    if (number > 0) {
-      if (line.type === GrDiffLine.Type.REMOVE) {
-        td.setAttribute('aria-label', `${number} removed`);
-      } else if (line.type === GrDiffLine.Type.ADD) {
-        td.setAttribute('aria-label', `${number} added`);
+  let columnPos = 0;
+  let textOffset = 0;
+  for (const segment of text.split(REGEX_TAB_OR_SURROGATE_PAIR)) {
+    if (segment) {
+      // |segment| contains only normal characters. If |segment| doesn't fit
+      // entirely on the current line, append chunks of |segment| followed by
+      // line breaks.
+      let rowStart = 0;
+      let rowEnd = lineLimit - columnPos;
+      while (rowEnd < segment.length) {
+        contentText.appendChild(
+            document.createTextNode(segment.substring(rowStart, rowEnd)));
+        contentText.appendChild(this._createElement('span', 'br'));
+        columnPos = 0;
+        rowStart = rowEnd;
+        rowEnd += lineLimit;
       }
+      // Append the last part of |segment|, which fits on the current line.
+      contentText.appendChild(
+          document.createTextNode(segment.substring(rowStart)));
+      columnPos += (segment.length - rowStart);
+      textOffset += segment.length;
     }
-
-    if (line.type === GrDiffLine.Type.BLANK) {
-      return td;
-    } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
-      td.classList.add('contextLineNum');
-    } else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
-      td.classList.add('lineNum');
-      td.setAttribute('data-value', number);
-      td.textContent = number === 'FILE' ? 'File' : number;
-    }
-    return td;
-  };
-
-  GrDiffBuilder.prototype._createTextEl = function(
-      lineNumberEl, line, opt_side) {
-    const td = this._createElement('td');
-    if (line.type !== GrDiffLine.Type.BLANK) {
-      td.classList.add('content');
-    }
-
-    // If intraline info is not available, the entire line will be
-    // considered as changed and marked as dark red / green color
-    if (!line.hasIntralineInfo) {
-      td.classList.add('no-intraline-info');
-    }
-    td.classList.add(line.type);
-
-    const lineLimit =
-        !this._prefs.line_wrapping ? this._prefs.line_length : Infinity;
-
-    const contentText =
-        this._formatText(line.text, this._prefs.tab_size, lineLimit);
-    if (opt_side) {
-      contentText.setAttribute('data-side', opt_side);
-    }
-
-    for (const layer of this.layers) {
-      if (typeof layer.annotate == 'function') {
-        layer.annotate(contentText, lineNumberEl, line);
-      }
-    }
-
-    td.appendChild(contentText);
-
-    return td;
-  };
-
-  /**
-   * Returns a 'div' element containing the supplied |text| as its innerText,
-   * with '\t' characters expanded to a width determined by |tabSize|, and the
-   * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
-   * desired.
-   *
-   * @param {string} text The text to be formatted.
-   * @param {number} tabSize The width of each tab stop.
-   * @param {number} lineLimit The column after which to wrap lines.
-   * @return {HTMLElement}
-   */
-  GrDiffBuilder.prototype._formatText = function(text, tabSize, lineLimit) {
-    const contentText = this._createElement('div', 'contentText');
-
-    let columnPos = 0;
-    let textOffset = 0;
-    for (const segment of text.split(REGEX_TAB_OR_SURROGATE_PAIR)) {
-      if (segment) {
-        // |segment| contains only normal characters. If |segment| doesn't fit
-        // entirely on the current line, append chunks of |segment| followed by
-        // line breaks.
-        let rowStart = 0;
-        let rowEnd = lineLimit - columnPos;
-        while (rowEnd < segment.length) {
-          contentText.appendChild(
-              document.createTextNode(segment.substring(rowStart, rowEnd)));
+    if (textOffset < text.length) {
+      // Handle the special character at |textOffset|.
+      if (text.startsWith('\t', textOffset)) {
+        // Append a single '\t' character.
+        let effectiveTabSize = tabSize - (columnPos % tabSize);
+        if (columnPos + effectiveTabSize > lineLimit) {
           contentText.appendChild(this._createElement('span', 'br'));
           columnPos = 0;
-          rowStart = rowEnd;
-          rowEnd += lineLimit;
+          effectiveTabSize = tabSize;
         }
-        // Append the last part of |segment|, which fits on the current line.
-        contentText.appendChild(
-            document.createTextNode(segment.substring(rowStart)));
-        columnPos += (segment.length - rowStart);
-        textOffset += segment.length;
-      }
-      if (textOffset < text.length) {
-        // Handle the special character at |textOffset|.
-        if (text.startsWith('\t', textOffset)) {
-          // Append a single '\t' character.
-          let effectiveTabSize = tabSize - (columnPos % tabSize);
-          if (columnPos + effectiveTabSize > lineLimit) {
-            contentText.appendChild(this._createElement('span', 'br'));
-            columnPos = 0;
-            effectiveTabSize = tabSize;
-          }
-          contentText.appendChild(this._getTabWrapper(effectiveTabSize));
-          columnPos += effectiveTabSize;
-          textOffset++;
-        } else {
-          // Append a single surrogate pair.
-          if (columnPos >= lineLimit) {
-            contentText.appendChild(this._createElement('span', 'br'));
-            columnPos = 0;
-          }
-          contentText.appendChild(document.createTextNode(
-              text.substring(textOffset, textOffset + 2)));
-          textOffset += 2;
-          columnPos += 1;
+        contentText.appendChild(this._getTabWrapper(effectiveTabSize));
+        columnPos += effectiveTabSize;
+        textOffset++;
+      } else {
+        // Append a single surrogate pair.
+        if (columnPos >= lineLimit) {
+          contentText.appendChild(this._createElement('span', 'br'));
+          columnPos = 0;
         }
+        contentText.appendChild(document.createTextNode(
+            text.substring(textOffset, textOffset + 2)));
+        textOffset += 2;
+        columnPos += 1;
       }
     }
-    return contentText;
-  };
+  }
+  return contentText;
+};
 
-  /**
-   * Returns a <span> element holding a '\t' character, that will visually
-   * occupy |tabSize| many columns.
-   *
-   * @param {number} tabSize The effective size of this tab stop.
-   * @return {HTMLElement}
-   */
-  GrDiffBuilder.prototype._getTabWrapper = function(tabSize) {
-    // Force this to be a number to prevent arbitrary injection.
-    const result = this._createElement('span', 'tab');
-    result.style['tab-size'] = tabSize;
-    result.style['-moz-tab-size'] = tabSize;
-    result.innerText = '\t';
-    return result;
-  };
+/**
+ * Returns a <span> element holding a '\t' character, that will visually
+ * occupy |tabSize| many columns.
+ *
+ * @param {number} tabSize The effective size of this tab stop.
+ * @return {HTMLElement}
+ */
+GrDiffBuilder.prototype._getTabWrapper = function(tabSize) {
+  // Force this to be a number to prevent arbitrary injection.
+  const result = this._createElement('span', 'tab');
+  result.style['tab-size'] = tabSize;
+  result.style['-moz-tab-size'] = tabSize;
+  result.innerText = '\t';
+  return result;
+};
 
-  GrDiffBuilder.prototype._createElement = function(tagName, classStr) {
-    const el = document.createElement(tagName);
-    // When Shady DOM is being used, these classes are added to account for
-    // Polymer's polyfill behavior. In order to guarantee sufficient
-    // specificity within the CSS rules, these are added to every element.
-    // Since the Polymer DOM utility functions (which would do this
-    // automatically) are not being used for performance reasons, this is
-    // done manually.
-    el.classList.add('style-scope', 'gr-diff');
-    if (classStr) {
-      for (const className of classStr.split(' ')) {
-        el.classList.add(className);
-      }
+GrDiffBuilder.prototype._createElement = function(tagName, classStr) {
+  const el = document.createElement(tagName);
+  // When Shady DOM is being used, these classes are added to account for
+  // Polymer's polyfill behavior. In order to guarantee sufficient
+  // specificity within the CSS rules, these are added to every element.
+  // Since the Polymer DOM utility functions (which would do this
+  // automatically) are not being used for performance reasons, this is
+  // done manually.
+  el.classList.add('style-scope', 'gr-diff');
+  if (classStr) {
+    for (const className of classStr.split(' ')) {
+      el.classList.add(className);
     }
-    return el;
-  };
+  }
+  return el;
+};
 
-  GrDiffBuilder.prototype._handleLayerUpdate = function(start, end, side) {
-    this._renderContentByRange(start, end, side);
-  };
+GrDiffBuilder.prototype._handleLayerUpdate = function(start, end, side) {
+  this._renderContentByRange(start, end, side);
+};
 
-  /**
-   * Finds the next DIV.contentText element following the given element, and on
-   * the same side. Will only search within a group.
-   *
-   * @param {HTMLElement} content
-   * @param {string} side Either 'left' or 'right'
-   * @return {HTMLElement}
-   */
-  GrDiffBuilder.prototype._getNextContentOnSide = function(content, side) {
-    throw Error('Subclasses must implement _getNextContentOnSide');
-  };
+/**
+ * Finds the next DIV.contentText element following the given element, and on
+ * the same side. Will only search within a group.
+ *
+ * @param {HTMLElement} content
+ * @param {string} side Either 'left' or 'right'
+ * @return {HTMLElement}
+ */
+GrDiffBuilder.prototype._getNextContentOnSide = function(content, side) {
+  throw Error('Subclasses must implement _getNextContentOnSide');
+};
 
-  /**
-   * Determines whether the given group is either totally an addition or totally
-   * a removal.
-   *
-   * @param {!Object} group (GrDiffGroup)
-   * @return {boolean}
-   */
-  GrDiffBuilder.prototype._isTotal = function(group) {
-    return group.type === GrDiffGroup.Type.DELTA &&
-        (!group.adds.length || !group.removes.length) &&
-        !(!group.adds.length && !group.removes.length);
-  };
+/**
+ * Determines whether the given group is either totally an addition or totally
+ * a removal.
+ *
+ * @param {!Object} group (GrDiffGroup)
+ * @return {boolean}
+ */
+GrDiffBuilder.prototype._isTotal = function(group) {
+  return group.type === GrDiffGroup.Type.DELTA &&
+      (!group.adds.length || !group.removes.length) &&
+      !(!group.adds.length && !group.removes.length);
+};
 
-  /**
-   * Set the blame information for the diff. For any already-rendered line,
-   * re-render its blame cell content.
-   *
-   * @param {Object} blame
-   */
-  GrDiffBuilder.prototype.setBlame = function(blame) {
-    this._blameInfo = blame;
+/**
+ * Set the blame information for the diff. For any already-rendered line,
+ * re-render its blame cell content.
+ *
+ * @param {Object} blame
+ */
+GrDiffBuilder.prototype.setBlame = function(blame) {
+  this._blameInfo = blame;
 
-    // TODO(wyatta): make this loop asynchronous.
-    for (const commit of blame) {
-      for (const range of commit.ranges) {
-        for (let i = range.start; i <= range.end; i++) {
-          // TODO(wyatta): this query is expensive, but, when traversing a
-          // range, the lines are consecutive, and given the previous blame
-          // cell, the next one can be reached cheaply.
-          const el = this._getBlameByLineNum(i);
-          if (!el) { continue; }
-          // Remove the element's children (if any).
-          while (el.hasChildNodes()) {
-            el.removeChild(el.lastChild);
-          }
-          const blame = this._getBlameForBaseLine(i, commit);
-          el.appendChild(blame);
+  // TODO(wyatta): make this loop asynchronous.
+  for (const commit of blame) {
+    for (const range of commit.ranges) {
+      for (let i = range.start; i <= range.end; i++) {
+        // TODO(wyatta): this query is expensive, but, when traversing a
+        // range, the lines are consecutive, and given the previous blame
+        // cell, the next one can be reached cheaply.
+        const el = this._getBlameByLineNum(i);
+        if (!el) { continue; }
+        // Remove the element's children (if any).
+        while (el.hasChildNodes()) {
+          el.removeChild(el.lastChild);
         }
+        const blame = this._getBlameForBaseLine(i, commit);
+        el.appendChild(blame);
       }
     }
-  };
+  }
+};
 
-  /**
-   * Find the blame cell for a given line number.
-   *
-   * @param {number} lineNum
-   * @return {HTMLTableDataCellElement}
-   */
-  GrDiffBuilder.prototype._getBlameByLineNum = function(lineNum) {
-    const root = Polymer.dom(this._outputEl);
-    return root.querySelector(`td.blame[data-line-number="${lineNum}"]`);
-  };
+/**
+ * Find the blame cell for a given line number.
+ *
+ * @param {number} lineNum
+ * @return {HTMLTableDataCellElement}
+ */
+GrDiffBuilder.prototype._getBlameByLineNum = function(lineNum) {
+  const root = Polymer.dom(this._outputEl);
+  return root.querySelector(`td.blame[data-line-number="${lineNum}"]`);
+};
 
-  /**
-   * Given a base line number, return the commit containing that line in the
-   * current set of blame information. If no blame information has been
-   * provided, null is returned.
-   *
-   * @param {number} lineNum
-   * @return {Object} The commit information.
-   */
-  GrDiffBuilder.prototype._getBlameCommitForBaseLine = function(lineNum) {
-    if (!this._blameInfo) { return null; }
+/**
+ * Given a base line number, return the commit containing that line in the
+ * current set of blame information. If no blame information has been
+ * provided, null is returned.
+ *
+ * @param {number} lineNum
+ * @return {Object} The commit information.
+ */
+GrDiffBuilder.prototype._getBlameCommitForBaseLine = function(lineNum) {
+  if (!this._blameInfo) { return null; }
 
-    for (const blameCommit of this._blameInfo) {
-      for (const range of blameCommit.ranges) {
-        if (range.start <= lineNum && range.end >= lineNum) {
-          return blameCommit;
-        }
+  for (const blameCommit of this._blameInfo) {
+    for (const range of blameCommit.ranges) {
+      if (range.start <= lineNum && range.end >= lineNum) {
+        return blameCommit;
       }
     }
-    return null;
-  };
+  }
+  return null;
+};
 
-  /**
-   * Given the number of a base line, get the content for the blame cell of that
-   * line. If there is no blame information for that line, returns null.
-   *
-   * @param {number} lineNum
-   * @param {Object=} opt_commit Optionally provide the commit object, so that
-   *     it does not need to be searched.
-   * @return {HTMLSpanElement}
-   */
-  GrDiffBuilder.prototype._getBlameForBaseLine = function(lineNum, opt_commit) {
-    const commit = opt_commit || this._getBlameCommitForBaseLine(lineNum);
-    if (!commit) { return null; }
+/**
+ * Given the number of a base line, get the content for the blame cell of that
+ * line. If there is no blame information for that line, returns null.
+ *
+ * @param {number} lineNum
+ * @param {Object=} opt_commit Optionally provide the commit object, so that
+ *     it does not need to be searched.
+ * @return {HTMLSpanElement}
+ */
+GrDiffBuilder.prototype._getBlameForBaseLine = function(lineNum, opt_commit) {
+  const commit = opt_commit || this._getBlameCommitForBaseLine(lineNum);
+  if (!commit) { return null; }
 
-    const isStartOfRange = commit.ranges.some(r => r.start === lineNum);
+  const isStartOfRange = commit.ranges.some(r => r.start === lineNum);
 
-    const date = (new Date(commit.time * 1000)).toLocaleDateString();
-    const blameNode = this._createElement('span',
-        isStartOfRange ? 'startOfRange' : '');
+  const date = (new Date(commit.time * 1000)).toLocaleDateString();
+  const blameNode = this._createElement('span',
+      isStartOfRange ? 'startOfRange' : '');
 
-    const shaNode = this._createElement('a', 'blameDate');
-    shaNode.innerText = `${date}`;
-    shaNode.setAttribute('href', `/q/${commit.id}`);
-    blameNode.appendChild(shaNode);
+  const shaNode = this._createElement('a', 'blameDate');
+  shaNode.innerText = `${date}`;
+  shaNode.setAttribute('href', `/q/${commit.id}`);
+  blameNode.appendChild(shaNode);
 
-    const shortName = commit.author.split(' ')[0];
-    const authorNode = this._createElement('span', 'blameAuthor');
-    authorNode.innerText = ` ${shortName}`;
-    blameNode.appendChild(authorNode);
+  const shortName = commit.author.split(' ')[0];
+  const authorNode = this._createElement('span', 'blameAuthor');
+  authorNode.innerText = ` ${shortName}`;
+  blameNode.appendChild(authorNode);
 
-    const hoverCardFragment = this._createElement('span', 'blameHoverCard');
-    hoverCardFragment.innerText =
-      `Commit ${commit.id}
+  const hoverCardFragment = this._createElement('span', 'blameHoverCard');
+  hoverCardFragment.innerText =
+    `Commit ${commit.id}
 Author: ${commit.author}
 Date: ${date}
 
 ${commit.commit_msg}`;
-    const hovercard = this._createElement('gr-hovercard');
-    hovercard.appendChild(hoverCardFragment);
-    blameNode.appendChild(hovercard);
+  const hovercard = this._createElement('gr-hovercard');
+  hovercard.appendChild(hoverCardFragment);
+  blameNode.appendChild(hovercard);
 
-    return blameNode;
-  };
+  return blameNode;
+};
 
-  /**
-   * Create a blame cell for the given base line. Blame information will be
-   * included in the cell if available.
-   *
-   * @param {GrDiffLine} line
-   * @return {HTMLTableDataCellElement}
-   */
-  GrDiffBuilder.prototype._createBlameCell = function(line) {
-    const blameTd = this._createElement('td', 'blame');
-    blameTd.setAttribute('data-line-number', line.beforeNumber);
-    if (line.beforeNumber) {
-      const content = this._getBlameForBaseLine(line.beforeNumber);
-      if (content) {
-        blameTd.appendChild(content);
-      }
+/**
+ * Create a blame cell for the given base line. Blame information will be
+ * included in the cell if available.
+ *
+ * @param {GrDiffLine} line
+ * @return {HTMLTableDataCellElement}
+ */
+GrDiffBuilder.prototype._createBlameCell = function(line) {
+  const blameTd = this._createElement('td', 'blame');
+  blameTd.setAttribute('data-line-number', line.beforeNumber);
+  if (line.beforeNumber) {
+    const content = this._getBlameForBaseLine(line.beforeNumber);
+    if (content) {
+      blameTd.appendChild(content);
     }
-    return blameTd;
-  };
+  }
+  return blameTd;
+};
 
-  /**
-   * Finds the line number element given the content element by walking up the
-   * DOM tree to the diff row and then querying for a .lineNum element on the
-   * requested side.
-   *
-   * TODO(brohlfs): Consolidate this with getLineEl... methods in html file.
-   */
-  GrDiffBuilder.prototype._getLineNumberEl = function(content, side) {
-    let row = content;
-    while (row && !row.classList.contains('diff-row')) row = row.parentElement;
-    return row ? row.querySelector('.lineNum.' + side) : null;
-  };
-
-  window.GrDiffBuilder = GrDiffBuilder;
-})(window, GrDiffGroup, GrDiffLine);
+/**
+ * Finds the line number element given the content element by walking up the
+ * DOM tree to the diff row and then querying for a .lineNum element on the
+ * requested side.
+ *
+ * TODO(brohlfs): Consolidate this with getLineEl... methods in html file.
+ */
+GrDiffBuilder.prototype._getLineNumberEl = function(content, side) {
+  let row = content;
+  while (row && !row.classList.contains('diff-row')) row = row.parentElement;
+  return row ? row.querySelector('.lineNum.' + side) : null;
+};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 3fb1d62..53b416b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -18,13 +18,13 @@
 
 import '../../../styles/shared-styles.js';
 import '../gr-selection-action-box/gr-selection-action-box.js';
-import './gr-range-normalizer.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-highlight_html.js';
 import {GrAnnotation} from './gr-annotation.js';
+import {GrRangeNormalizer} from './gr-range-normalizer.js';
 
 /**
  * @extends Polymer.Element
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index c4f7152..fe8d382 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -144,6 +144,8 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import './gr-diff-highlight.js';
+import {GrRangeNormalizer} from './gr-range-normalizer.js';
+
 suite('gr-diff-highlight', () => {
   let element;
   let sandbox;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
index 3b6c41c..5d04bd7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
@@ -14,99 +14,91 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  // Prevent redefinition.
-  if (window.GrRangeNormalizer) { return; }
+// Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
+const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
 
-  // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
-  const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+export const GrRangeNormalizer = {
+  /**
+   * Remap DOM range to whole lines of a diff if necessary. If the start or
+   * end containers are DOM elements that are singular pieces of syntax
+   * highlighting, the containers are remapped to the .contentText divs that
+   * contain the entire line of code.
+   *
+   * @param {!Object} range - the standard DOM selector range.
+   * @return {!Object} A modified version of the range that correctly accounts
+   *     for syntax highlighting.
+   */
+  normalize(range) {
+    const startContainer = this._getContentTextParent(range.startContainer);
+    const startOffset = range.startOffset +
+        this._getTextOffset(startContainer, range.startContainer);
+    const endContainer = this._getContentTextParent(range.endContainer);
+    const endOffset = range.endOffset + this._getTextOffset(endContainer,
+        range.endContainer);
+    return {
+      startContainer,
+      startOffset,
+      endContainer,
+      endOffset,
+    };
+  },
 
-  const GrRangeNormalizer = {
-    /**
-     * Remap DOM range to whole lines of a diff if necessary. If the start or
-     * end containers are DOM elements that are singular pieces of syntax
-     * highlighting, the containers are remapped to the .contentText divs that
-     * contain the entire line of code.
-     *
-     * @param {!Object} range - the standard DOM selector range.
-     * @return {!Object} A modified version of the range that correctly accounts
-     *     for syntax highlighting.
-     */
-    normalize(range) {
-      const startContainer = this._getContentTextParent(range.startContainer);
-      const startOffset = range.startOffset +
-          this._getTextOffset(startContainer, range.startContainer);
-      const endContainer = this._getContentTextParent(range.endContainer);
-      const endOffset = range.endOffset + this._getTextOffset(endContainer,
-          range.endContainer);
-      return {
-        startContainer,
-        startOffset,
-        endContainer,
-        endOffset,
-      };
-    },
-
-    _getContentTextParent(target) {
-      let element = target;
-      if (element.nodeName === '#text') {
-        element = element.parentElement;
+  _getContentTextParent(target) {
+    let element = target;
+    if (element.nodeName === '#text') {
+      element = element.parentElement;
+    }
+    while (element && !element.classList.contains('contentText')) {
+      if (element.parentElement === null) {
+        return target;
       }
-      while (element && !element.classList.contains('contentText')) {
-        if (element.parentElement === null) {
-          return target;
-        }
-        element = element.parentElement;
+      element = element.parentElement;
+    }
+    return element;
+  },
+
+  /**
+   * Gets the character offset of the child within the parent.
+   * Performs a synchronous in-order traversal from top to bottom of the node
+   * element, counting the length of the syntax until child is found.
+   *
+   * @param {!Element} node The root DOM element to be searched through.
+   * @param {!Element} child The child element being searched for.
+   * @return {number}
+   */
+  _getTextOffset(node, child) {
+    let count = 0;
+    let stack = [node];
+    while (stack.length) {
+      const n = stack.pop();
+      if (n === child) {
+        break;
       }
-      return element;
-    },
-
-    /**
-     * Gets the character offset of the child within the parent.
-     * Performs a synchronous in-order traversal from top to bottom of the node
-     * element, counting the length of the syntax until child is found.
-     *
-     * @param {!Element} node The root DOM element to be searched through.
-     * @param {!Element} child The child element being searched for.
-     * @return {number}
-     */
-    _getTextOffset(node, child) {
-      let count = 0;
-      let stack = [node];
-      while (stack.length) {
-        const n = stack.pop();
-        if (n === child) {
-          break;
+      if (n && n.childNodes && n.childNodes.length !== 0) {
+        const arr = [];
+        for (const childNode of n.childNodes) {
+          arr.push(childNode);
         }
-        if (n && n.childNodes && n.childNodes.length !== 0) {
-          const arr = [];
-          for (const childNode of n.childNodes) {
-            arr.push(childNode);
-          }
-          arr.reverse();
-          stack = stack.concat(arr);
-        } else {
-          count += this._getLength(n);
-        }
+        arr.reverse();
+        stack = stack.concat(arr);
+      } else {
+        count += this._getLength(n);
       }
-      return count;
-    },
+    }
+    return count;
+  },
 
-    /**
-     * The DOM API textContent.length calculation is broken when the text
-     * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
-     *
-     * @param {text} node A text node.
-     * @return {number} The length of the text.
-     */
-    _getLength(node) {
-      return node ?
-        node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length :
-        0;
-    },
-  };
-
-  window.GrRangeNormalizer = GrRangeNormalizer;
-})(window);
+  /**
+   * The DOM API textContent.length calculation is broken when the text
+   * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
+   *
+   * @param {text} node A text node.
+   * @return {number} The length of the text.
+   */
+  _getLength(node) {
+    return node ?
+      node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length :
+      0;
+  },
+};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index 6936957..f281785 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -29,6 +29,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-host_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import {GrDiffBuilder} from '../gr-diff-builder/gr-diff-builder.js';
 
 const MSG_EMPTY_BLAME = 'No blame information for this diff.';
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index f84e1c7..bf32f34 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -33,6 +33,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import './gr-diff-host.js';
+import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 suite('gr-diff-host tests', () => {
   let element;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index adcb375..ac34990 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -16,12 +16,12 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../gr-diff/gr-diff-line.js';
-import '../gr-diff/gr-diff-group.js';
 import '../../../scripts/util.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
+import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
 
 const WHOLE_FILE = -1;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 63209c1..5577b8c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -33,6 +33,9 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import './gr-diff-processor.js';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
+import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
+
 suite('gr-diff-processor tests', () => {
   const WHOLE_FILE = -1;
   const loremIpsum =
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index a8ba78c..3fa3899 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -18,7 +18,6 @@
 
 import '../../../styles/shared-styles.js';
 import '../../../scripts/util.js';
-import '../gr-diff-highlight/gr-range-normalizer.js';
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -27,6 +26,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-selection_html.js';
 import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
+import {GrRangeNormalizer} from '../gr-diff-highlight/gr-range-normalizer.js';
 
 /**
  * Possible CSS classes indicating the state of selection. Dynamically added/
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 9f82d81..e87b25c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -22,7 +22,6 @@
 import '../../core/gr-navigation/gr-navigation.js';
 import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
 import '../../shared/gr-fixed-panel/gr-fixed-panel.js';
@@ -47,6 +46,7 @@
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 
 const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
 const MSG_LOADING_BLAME = 'Loading blame...';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
index c62c603..bfd063a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -14,276 +14,269 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
+import {GrDiffLine} from './gr-diff-line.js';
 
-  // Prevent redefinition.
-  if (window.GrDiffGroup) { return; }
+/**
+ * A chunk of the diff that should be rendered together.
+ *
+ * @constructor
+ * @param {!GrDiffGroup.Type} type
+ * @param {!Array<!GrDiffLine>=} opt_lines
+ */
+export function GrDiffGroup(type, opt_lines) {
+  /** @type {!GrDiffGroup.Type} */
+  this.type = type;
+
+  /** @type {boolean} */
+  this.dueToRebase = false;
 
   /**
-   * A chunk of the diff that should be rendered together.
+   * True means all changes in this line are whitespace changes that should
+   * not be highlighted as changed as per the user settings.
    *
-   * @constructor
-   * @param {!GrDiffGroup.Type} type
-   * @param {!Array<!GrDiffLine>=} opt_lines
+   * @type{boolean}
    */
-  function GrDiffGroup(type, opt_lines) {
-    /** @type {!GrDiffGroup.Type} */
-    this.type = type;
+  this.ignoredWhitespaceOnly = false;
 
-    /** @type {boolean} */
-    this.dueToRebase = false;
+  /**
+   * True means it should not be collapsed (because it was in the URL, or
+   * there is a comment on that line)
+   */
+  this.keyLocation = false;
 
-    /**
-     * True means all changes in this line are whitespace changes that should
-     * not be highlighted as changed as per the user settings.
-     *
-     * @type{boolean}
-     */
-    this.ignoredWhitespaceOnly = false;
+  /** @type {?HTMLElement} */
+  this.element = null;
 
-    /**
-     * True means it should not be collapsed (because it was in the URL, or
-     * there is a comment on that line)
-     */
-    this.keyLocation = false;
+  /** @type {!Array<!GrDiffLine>} */
+  this.lines = [];
+  /** @type {!Array<!GrDiffLine>} */
+  this.adds = [];
+  /** @type {!Array<!GrDiffLine>} */
+  this.removes = [];
 
-    /** @type {?HTMLElement} */
-    this.element = null;
+  /** Both start and end line are inclusive. */
+  this.lineRange = {
+    left: {start: null, end: null},
+    right: {start: null, end: null},
+  };
 
-    /** @type {!Array<!GrDiffLine>} */
-    this.lines = [];
-    /** @type {!Array<!GrDiffLine>} */
-    this.adds = [];
-    /** @type {!Array<!GrDiffLine>} */
-    this.removes = [];
+  if (opt_lines) {
+    opt_lines.forEach(this.addLine, this);
+  }
+}
 
-    /** Both start and end line are inclusive. */
-    this.lineRange = {
-      left: {start: null, end: null},
-      right: {start: null, end: null},
-    };
+/** @enum {string} */
+GrDiffGroup.Type = {
+  /** Unchanged context. */
+  BOTH: 'both',
 
-    if (opt_lines) {
-      opt_lines.forEach(this.addLine, this);
+  /** A widget used to show more context. */
+  CONTEXT_CONTROL: 'contextControl',
+
+  /** Added, removed or modified chunk. */
+  DELTA: 'delta',
+};
+
+/**
+ * Hides lines in the given range behind a context control group.
+ *
+ * Groups that would be partially visible are split into their visible and
+ * hidden parts, respectively.
+ * The groups need to be "common groups", meaning they have to have either
+ * originated from an `ab` chunk, or from an `a`+`b` chunk with
+ * `common: true`.
+ *
+ * If the hidden range is 1 line or less, nothing is hidden and no context
+ * control group is created.
+ *
+ * @param {!Array<!GrDiffGroup>} groups Common groups, ordered by their line
+ *     ranges.
+ * @param {number} hiddenStart The first element to be hidden, as a
+ *     non-negative line number offset relative to the first group's start
+ *     line, left and right respectively.
+ * @param {number} hiddenEnd The first visible element after the hidden range,
+ *     as a non-negative line number offset relative to the first group's
+ *     start line, left and right respectively.
+ * @return {!Array<!GrDiffGroup>}
+ */
+GrDiffGroup.hideInContextControl = function(groups, hiddenStart, hiddenEnd) {
+  if (groups.length === 0) return [];
+  // Clamp hiddenStart and hiddenEnd - inspired by e.g. substring
+  hiddenStart = Math.max(hiddenStart, 0);
+  hiddenEnd = Math.max(hiddenEnd, hiddenStart);
+
+  let before = [];
+  let hidden = groups;
+  let after = [];
+
+  const numHidden = hiddenEnd - hiddenStart;
+
+  // Only collapse if there is more than 1 line to be hidden.
+  if (numHidden > 1) {
+    if (hiddenStart) {
+      [before, hidden] = GrDiffGroup._splitCommonGroups(hidden, hiddenStart);
+    }
+    if (hiddenEnd) {
+      [hidden, after] = GrDiffGroup._splitCommonGroups(
+          hidden, hiddenEnd - hiddenStart);
+    }
+  } else {
+    [hidden, after] = [[], hidden];
+  }
+
+  const result = [...before];
+  if (hidden.length) {
+    const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
+    ctxLine.contextGroups = hidden;
+    const ctxGroup = new GrDiffGroup(
+        GrDiffGroup.Type.CONTEXT_CONTROL, [ctxLine]);
+    result.push(ctxGroup);
+  }
+  result.push(...after);
+  return result;
+};
+
+/**
+ * Splits a list of common groups into two lists of groups.
+ *
+ * Groups where all lines are before or all lines are after the split will be
+ * retained as is and put into the first or second list respectively. Groups
+ * with some lines before and some lines after the split will be split into
+ * two groups, which will be put into the first and second list.
+ *
+ * @param {!Array<!GrDiffGroup>} groups
+ * @param {number} split A line number offset relative to the first group's
+ *     start line at which the groups should be split.
+ * @return {!Array<!Array<!GrDiffGroup>>} The outer array has 2 elements, the
+ *   list of groups before and the list of groups after the split.
+ */
+GrDiffGroup._splitCommonGroups = function(groups, split) {
+  if (groups.length === 0) return [[], []];
+  const leftSplit = groups[0].lineRange.left.start + split;
+  const rightSplit = groups[0].lineRange.right.start + split;
+
+  const beforeGroups = [];
+  const afterGroups = [];
+  for (const group of groups) {
+    if (group.lineRange.left.end < leftSplit ||
+        group.lineRange.right.end < rightSplit) {
+      beforeGroups.push(group);
+      continue;
+    }
+    if (leftSplit <= group.lineRange.left.start ||
+        rightSplit <= group.lineRange.right.start) {
+      afterGroups.push(group);
+      continue;
+    }
+
+    const before = [];
+    const after = [];
+    for (const line of group.lines) {
+      if ((line.beforeNumber && line.beforeNumber < leftSplit) ||
+          (line.afterNumber && line.afterNumber < rightSplit)) {
+        before.push(line);
+      } else {
+        after.push(line);
+      }
+    }
+
+    if (before.length) {
+      beforeGroups.push(before.length === group.lines.length ?
+        group : group.cloneWithLines(before));
+    }
+    if (after.length) {
+      afterGroups.push(after.length === group.lines.length ?
+        group : group.cloneWithLines(after));
+    }
+  }
+  return [beforeGroups, afterGroups];
+};
+
+/**
+ * Creates a new group with the same properties but different lines.
+ *
+ * The element property is not copied, because the original element is still a
+ * rendering of the old lines, so that would not make sense.
+ *
+ * @param {!Array<!GrDiffLine>} lines
+ * @return {!GrDiffGroup}
+ */
+GrDiffGroup.prototype.cloneWithLines = function(lines) {
+  const group = new GrDiffGroup(this.type, lines);
+  group.dueToRebase = this.dueToRebase;
+  group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly;
+  return group;
+};
+
+/** @param {!GrDiffLine} line */
+GrDiffGroup.prototype.addLine = function(line) {
+  this.lines.push(line);
+
+  const notDelta = (this.type === GrDiffGroup.Type.BOTH ||
+      this.type === GrDiffGroup.Type.CONTEXT_CONTROL);
+  if (notDelta && (line.type === GrDiffLine.Type.ADD ||
+      line.type === GrDiffLine.Type.REMOVE)) {
+    throw Error('Cannot add delta line to a non-delta group.');
+  }
+
+  if (line.type === GrDiffLine.Type.ADD) {
+    this.adds.push(line);
+  } else if (line.type === GrDiffLine.Type.REMOVE) {
+    this.removes.push(line);
+  }
+  this._updateRange(line);
+};
+
+/** @return {!Array<{left: GrDiffLine, right: GrDiffLine}>} */
+GrDiffGroup.prototype.getSideBySidePairs = function() {
+  if (this.type === GrDiffGroup.Type.BOTH ||
+      this.type === GrDiffGroup.Type.CONTEXT_CONTROL) {
+    return this.lines.map(line => {
+      return {
+        left: line,
+        right: line,
+      };
+    });
+  }
+
+  const pairs = [];
+  let i = 0;
+  let j = 0;
+  while (i < this.removes.length || j < this.adds.length) {
+    pairs.push({
+      left: this.removes[i] || GrDiffLine.BLANK_LINE,
+      right: this.adds[j] || GrDiffLine.BLANK_LINE,
+    });
+    i++;
+    j++;
+  }
+  return pairs;
+};
+
+GrDiffGroup.prototype._updateRange = function(line) {
+  if (line.beforeNumber === 'FILE' || line.afterNumber === 'FILE') { return; }
+
+  if (line.type === GrDiffLine.Type.ADD ||
+      line.type === GrDiffLine.Type.BOTH) {
+    if (this.lineRange.right.start === null ||
+        line.afterNumber < this.lineRange.right.start) {
+      this.lineRange.right.start = line.afterNumber;
+    }
+    if (this.lineRange.right.end === null ||
+        line.afterNumber > this.lineRange.right.end) {
+      this.lineRange.right.end = line.afterNumber;
     }
   }
 
-  /** @enum {string} */
-  GrDiffGroup.Type = {
-    /** Unchanged context. */
-    BOTH: 'both',
-
-    /** A widget used to show more context. */
-    CONTEXT_CONTROL: 'contextControl',
-
-    /** Added, removed or modified chunk. */
-    DELTA: 'delta',
-  };
-
-  /**
-   * Hides lines in the given range behind a context control group.
-   *
-   * Groups that would be partially visible are split into their visible and
-   * hidden parts, respectively.
-   * The groups need to be "common groups", meaning they have to have either
-   * originated from an `ab` chunk, or from an `a`+`b` chunk with
-   * `common: true`.
-   *
-   * If the hidden range is 1 line or less, nothing is hidden and no context
-   * control group is created.
-   *
-   * @param {!Array<!GrDiffGroup>} groups Common groups, ordered by their line
-   *     ranges.
-   * @param {number} hiddenStart The first element to be hidden, as a
-   *     non-negative line number offset relative to the first group's start
-   *     line, left and right respectively.
-   * @param {number} hiddenEnd The first visible element after the hidden range,
-   *     as a non-negative line number offset relative to the first group's
-   *     start line, left and right respectively.
-   * @return {!Array<!GrDiffGroup>}
-   */
-  GrDiffGroup.hideInContextControl = function(groups, hiddenStart, hiddenEnd) {
-    if (groups.length === 0) return [];
-    // Clamp hiddenStart and hiddenEnd - inspired by e.g. substring
-    hiddenStart = Math.max(hiddenStart, 0);
-    hiddenEnd = Math.max(hiddenEnd, hiddenStart);
-
-    let before = [];
-    let hidden = groups;
-    let after = [];
-
-    const numHidden = hiddenEnd - hiddenStart;
-
-    // Only collapse if there is more than 1 line to be hidden.
-    if (numHidden > 1) {
-      if (hiddenStart) {
-        [before, hidden] = GrDiffGroup._splitCommonGroups(hidden, hiddenStart);
-      }
-      if (hiddenEnd) {
-        [hidden, after] = GrDiffGroup._splitCommonGroups(
-            hidden, hiddenEnd - hiddenStart);
-      }
-    } else {
-      [hidden, after] = [[], hidden];
+  if (line.type === GrDiffLine.Type.REMOVE ||
+      line.type === GrDiffLine.Type.BOTH) {
+    if (this.lineRange.left.start === null ||
+        line.beforeNumber < this.lineRange.left.start) {
+      this.lineRange.left.start = line.beforeNumber;
     }
-
-    const result = [...before];
-    if (hidden.length) {
-      const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
-      ctxLine.contextGroups = hidden;
-      const ctxGroup = new GrDiffGroup(
-          GrDiffGroup.Type.CONTEXT_CONTROL, [ctxLine]);
-      result.push(ctxGroup);
+    if (this.lineRange.left.end === null ||
+        line.beforeNumber > this.lineRange.left.end) {
+      this.lineRange.left.end = line.beforeNumber;
     }
-    result.push(...after);
-    return result;
-  };
-
-  /**
-   * Splits a list of common groups into two lists of groups.
-   *
-   * Groups where all lines are before or all lines are after the split will be
-   * retained as is and put into the first or second list respectively. Groups
-   * with some lines before and some lines after the split will be split into
-   * two groups, which will be put into the first and second list.
-   *
-   * @param {!Array<!GrDiffGroup>} groups
-   * @param {number} split A line number offset relative to the first group's
-   *     start line at which the groups should be split.
-   * @return {!Array<!Array<!GrDiffGroup>>} The outer array has 2 elements, the
-   *   list of groups before and the list of groups after the split.
-   */
-  GrDiffGroup._splitCommonGroups = function(groups, split) {
-    if (groups.length === 0) return [[], []];
-    const leftSplit = groups[0].lineRange.left.start + split;
-    const rightSplit = groups[0].lineRange.right.start + split;
-
-    const beforeGroups = [];
-    const afterGroups = [];
-    for (const group of groups) {
-      if (group.lineRange.left.end < leftSplit ||
-          group.lineRange.right.end < rightSplit) {
-        beforeGroups.push(group);
-        continue;
-      }
-      if (leftSplit <= group.lineRange.left.start ||
-          rightSplit <= group.lineRange.right.start) {
-        afterGroups.push(group);
-        continue;
-      }
-
-      const before = [];
-      const after = [];
-      for (const line of group.lines) {
-        if ((line.beforeNumber && line.beforeNumber < leftSplit) ||
-            (line.afterNumber && line.afterNumber < rightSplit)) {
-          before.push(line);
-        } else {
-          after.push(line);
-        }
-      }
-
-      if (before.length) {
-        beforeGroups.push(before.length === group.lines.length ?
-          group : group.cloneWithLines(before));
-      }
-      if (after.length) {
-        afterGroups.push(after.length === group.lines.length ?
-          group : group.cloneWithLines(after));
-      }
-    }
-    return [beforeGroups, afterGroups];
-  };
-
-  /**
-   * Creates a new group with the same properties but different lines.
-   *
-   * The element property is not copied, because the original element is still a
-   * rendering of the old lines, so that would not make sense.
-   *
-   * @param {!Array<!GrDiffLine>} lines
-   * @return {!GrDiffGroup}
-   */
-  GrDiffGroup.prototype.cloneWithLines = function(lines) {
-    const group = new GrDiffGroup(this.type, lines);
-    group.dueToRebase = this.dueToRebase;
-    group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly;
-    return group;
-  };
-
-  /** @param {!GrDiffLine} line */
-  GrDiffGroup.prototype.addLine = function(line) {
-    this.lines.push(line);
-
-    const notDelta = (this.type === GrDiffGroup.Type.BOTH ||
-        this.type === GrDiffGroup.Type.CONTEXT_CONTROL);
-    if (notDelta && (line.type === GrDiffLine.Type.ADD ||
-        line.type === GrDiffLine.Type.REMOVE)) {
-      throw Error('Cannot add delta line to a non-delta group.');
-    }
-
-    if (line.type === GrDiffLine.Type.ADD) {
-      this.adds.push(line);
-    } else if (line.type === GrDiffLine.Type.REMOVE) {
-      this.removes.push(line);
-    }
-    this._updateRange(line);
-  };
-
-  /** @return {!Array<{left: GrDiffLine, right: GrDiffLine}>} */
-  GrDiffGroup.prototype.getSideBySidePairs = function() {
-    if (this.type === GrDiffGroup.Type.BOTH ||
-        this.type === GrDiffGroup.Type.CONTEXT_CONTROL) {
-      return this.lines.map(line => {
-        return {
-          left: line,
-          right: line,
-        };
-      });
-    }
-
-    const pairs = [];
-    let i = 0;
-    let j = 0;
-    while (i < this.removes.length || j < this.adds.length) {
-      pairs.push({
-        left: this.removes[i] || GrDiffLine.BLANK_LINE,
-        right: this.adds[j] || GrDiffLine.BLANK_LINE,
-      });
-      i++;
-      j++;
-    }
-    return pairs;
-  };
-
-  GrDiffGroup.prototype._updateRange = function(line) {
-    if (line.beforeNumber === 'FILE' || line.afterNumber === 'FILE') { return; }
-
-    if (line.type === GrDiffLine.Type.ADD ||
-        line.type === GrDiffLine.Type.BOTH) {
-      if (this.lineRange.right.start === null ||
-          line.afterNumber < this.lineRange.right.start) {
-        this.lineRange.right.start = line.afterNumber;
-      }
-      if (this.lineRange.right.end === null ||
-          line.afterNumber > this.lineRange.right.end) {
-        this.lineRange.right.end = line.afterNumber;
-      }
-    }
-
-    if (line.type === GrDiffLine.Type.REMOVE ||
-        line.type === GrDiffLine.Type.BOTH) {
-      if (this.lineRange.left.start === null ||
-          line.beforeNumber < this.lineRange.left.start) {
-        this.lineRange.left.start = line.beforeNumber;
-      }
-      if (this.lineRange.left.end === null ||
-          line.beforeNumber > this.lineRange.left.end) {
-        this.lineRange.left.end = line.beforeNumber;
-      }
-    }
-  };
-
-  window.GrDiffGroup = GrDiffGroup;
-})(window);
+  }
+};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
index cd43803..12d7324 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -24,8 +24,9 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script type="module">
 import '../../../test/common-test-setup.js';
-import './gr-diff-line.js';
-import './gr-diff-group.js';
+import {GrDiffLine} from './gr-diff-line.js';
+import {GrDiffGroup} from './gr-diff-group.js';
+
 suite('gr-diff-group tests', () => {
   test('delta line pairs', () => {
     let group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
index 42926ab..70387ca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -14,68 +14,60 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  // Prevent redefinition.
-  if (window.GrDiffLine) { return; }
+/**
+ * @constructor
+ * @param {GrDiffLine.Type} type
+ * @param {number|string=} opt_beforeLine
+ * @param {number|string=} opt_afterLine
+ */
+export function GrDiffLine(type, opt_beforeLine, opt_afterLine) {
+  this.type = type;
 
-  /**
-   * @constructor
-   * @param {GrDiffLine.Type} type
-   * @param {number|string=} opt_beforeLine
-   * @param {number|string=} opt_afterLine
-   */
-  function GrDiffLine(type, opt_beforeLine, opt_afterLine) {
-    this.type = type;
+  /** @type {number|string} */
+  this.beforeNumber = opt_beforeLine || 0;
 
-    /** @type {number|string} */
-    this.beforeNumber = opt_beforeLine || 0;
+  /** @type {number|string} */
+  this.afterNumber = opt_afterLine || 0;
 
-    /** @type {number|string} */
-    this.afterNumber = opt_afterLine || 0;
+  /** @type {boolean} */
+  this.hasIntralineInfo = false;
 
-    /** @type {boolean} */
-    this.hasIntralineInfo = false;
+  /** @type {!Array<GrDiffLine.Highlights>} */
+  this.highlights = [];
 
-    /** @type {!Array<GrDiffLine.Highlights>} */
-    this.highlights = [];
+  /** @type {?Array<Object>} ?Array<!GrDiffGroup> */
+  this.contextGroups = null;
 
-    /** @type {?Array<Object>} ?Array<!GrDiffGroup> */
-    this.contextGroups = null;
+  this.text = '';
+}
 
-    this.text = '';
-  }
+/** @enum {string} */
+GrDiffLine.Type = {
+  ADD: 'add',
+  BOTH: 'both',
+  BLANK: 'blank',
+  CONTEXT_CONTROL: 'contextControl',
+  REMOVE: 'remove',
+};
 
-  /** @enum {string} */
-  GrDiffLine.Type = {
-    ADD: 'add',
-    BOTH: 'both',
-    BLANK: 'blank',
-    CONTEXT_CONTROL: 'contextControl',
-    REMOVE: 'remove',
-  };
+/**
+ * A line highlight object consists of three fields:
+ * - contentIndex: The index of the chunk `content` field (the line
+ *   being referred to).
+ * - startIndex: Index of the character where the highlight should begin.
+ * - endIndex: (optional) Index of the character where the highlight should
+ *   end. If omitted, the highlight is meant to be a continuation onto the
+ *   next line.
+ *
+ * @typedef {{
+ *  contentIndex: number,
+ *  startIndex: number,
+ *  endIndex: number
+ * }}
+ */
+GrDiffLine.Highlights;
 
-  /**
-   * A line highlight object consists of three fields:
-   * - contentIndex: The index of the chunk `content` field (the line
-   *   being referred to).
-   * - startIndex: Index of the character where the highlight should begin.
-   * - endIndex: (optional) Index of the character where the highlight should
-   *   end. If omitted, the highlight is meant to be a continuation onto the
-   *   next line.
-   *
-   * @typedef {{
-   *  contentIndex: number,
-   *  startIndex: number,
-   *  endIndex: number
-   * }}
-   */
-  GrDiffLine.Highlights;
+GrDiffLine.FILE = 'FILE';
 
-  GrDiffLine.FILE = 'FILE';
-
-  GrDiffLine.BLANK_LINE = new GrDiffLine(GrDiffLine.Type.BLANK);
-
-  window.GrDiffLine = GrDiffLine;
-})(window);
+GrDiffLine.BLANK_LINE = new GrDiffLine(GrDiffLine.Type.BLANK);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 189d0f1..0597994 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -24,8 +24,6 @@
 import '../gr-syntax-themes/gr-syntax-theme.js';
 import '../gr-ranged-comment-themes/gr-ranged-comment-theme.js';
 import '../../../scripts/hiddenscroll.js';
-import './gr-diff-line.js';
-import './gr-diff-group.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -33,6 +31,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {htmlTemplate} from './gr-diff_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import {GrDiffLine} from './gr-diff-line.js';
 
 const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
 const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 099f32b..6f3f060 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -38,6 +38,8 @@
 import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
 import './gr-diff.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
+
 suite('gr-diff tests', () => {
   let element;
   let sandbox;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index badac03..8bdc1a8 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -18,7 +18,6 @@
 
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
-import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import '../../shared/gr-select/gr-select.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -27,6 +26,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-patch-range-select_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index 1b6d288..e6c49be 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -21,6 +21,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-ranged-comment-layer_html.js';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 
 // Polymer 1 adds # before array's key, while Polymer 2 doesn't
 const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/;
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
index 873c50b..eec6125 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -35,6 +35,7 @@
 import '../gr-diff/gr-diff-line.js';
 import './gr-ranged-comment-layer.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 
 suite('gr-ranged-comment-layer', () => {
   let element;
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index cee7b34..75a1f60 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -18,12 +18,12 @@
 
 import '../../shared/gr-lib-loader/gr-lib-loader.js';
 import '../../../scripts/util.js';
-import '../gr-diff/gr-diff-line.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-syntax-layer_html.js';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 
 const LANGUAGE_MAP = {
   'application/dart': 'dart',
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 6c2a405..36af6ae 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -35,6 +35,7 @@
 import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
 import './gr-syntax-layer.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
+import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 
 suite('gr-syntax-layer tests', () => {
   let sandbox;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-constants.js b/polygerrit-ui/app/elements/edit/gr-edit-constants.js
index 2a929f2..444cde0 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-constants.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-constants.js
@@ -14,18 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  const GrEditConstants = window.GrEditConstants || {};
-
-  // Order corresponds to order in the UI.
-  GrEditConstants.Actions = {
+export const GrEditConstants = {
+// Order corresponds to order in the UI.
+  Actions: {
     OPEN: {label: 'Add/Open', id: 'open'},
     DELETE: {label: 'Delete', id: 'delete'},
     RENAME: {label: 'Rename', id: 'rename'},
     RESTORE: {label: 'Restore', id: 'restore'},
-  };
+  },
+};
 
-  window.GrEditConstants = GrEditConstants;
-})(window);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index b2e0c60..7d4dac8 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -24,7 +24,6 @@
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-edit-constants.js';
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -33,6 +32,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-edit-controls_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import {GrEditConstants} from '../gr-edit-constants.js';
 
 /**
  * @extends Polymer.Element
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index 10bff3c..8a24e23 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -18,12 +18,12 @@
 
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
-import '../gr-edit-constants.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-edit-file-controls_html.js';
+import {GrEditConstants} from '../gr-edit-constants.js';
 
 /** @extends Polymer.Element */
 class GrEditFileControls extends GestureEventListeners(
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
index 46a336f..94e18ff 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -33,6 +33,8 @@
 import '../../../test/common-test-setup.js';
 import '../gr-edit-constants.js';
 import './gr-edit-file-controls.js';
+import {GrEditConstants} from '../gr-edit-constants.js';
+
 suite('gr-edit-file-controls tests', () => {
   let element;
   let sandbox;
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.js b/polygerrit-ui/app/elements/gr-app-global-var-init.js
index 1f33fd5..4e6f320 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.js
@@ -24,8 +24,57 @@
 
 import {GrDisplayNameUtils} from '../scripts/gr-display-name-utils/gr-display-name-utils.js';
 import {GrAnnotation} from './diff/gr-diff-highlight/gr-annotation.js';
+import {GrAttributeHelper} from './plugins/gr-attribute-helper/gr-attribute-helper.js';
+import {GrDiffLine} from './diff/gr-diff/gr-diff-line.js';
+import {GrDiffGroup} from './diff/gr-diff/gr-diff-group.js';
+import {GrDiffBuilder} from './diff/gr-diff-builder/gr-diff-builder.js';
+import {GrDiffBuilderSideBySide} from './diff/gr-diff-builder/gr-diff-builder-side-by-side.js';
+import {GrDiffBuilderImage} from './diff/gr-diff-builder/gr-diff-builder-image.js';
+import {GrDiffBuilderUnified} from './diff/gr-diff-builder/gr-diff-builder-unified.js';
+import {GrDiffBuilderBinary} from './diff/gr-diff-builder/gr-diff-builder-binary.js';
+import {GrChangeActionsInterface} from './shared/gr-js-api-interface/gr-change-actions-js-api.js';
+import {GrChangeReplyInterface} from './shared/gr-js-api-interface/gr-change-reply-js-api.js';
+import {GrEditConstants} from './edit/gr-edit-constants.js';
+import {GrFileListConstants} from './change/gr-file-list-constants.js';
+import {GrDomHooksManager, GrDomHook} from './plugins/gr-dom-hooks/gr-dom-hooks.js';
+import {GrEtagDecorator} from './shared/gr-rest-api-interface/gr-etag-decorator.js';
+import {GrThemeApi} from './plugins/gr-theme-api/gr-theme-api.js';
+import {SiteBasedCache, FetchPromisesCache, GrRestApiHelper} from './shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js';
+import {GrLinkTextParser} from './shared/gr-linked-text/link-text-parser.js';
+import {GrPluginEndpoints} from './shared/gr-js-api-interface/gr-plugin-endpoints.js';
+import {GrReviewerUpdatesParser} from './shared/gr-rest-api-interface/gr-reviewer-updates-parser.js';
+import {GrPopupInterface} from './plugins/gr-popup-interface/gr-popup-interface.js';
+import {GrRangeNormalizer} from './diff/gr-diff-highlight/gr-range-normalizer.js';
+import {GrCountStringFormatter} from './shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import {GrReviewerSuggestionsProvider} from '../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 
 export function initGlobalVariables() {
   window.GrDisplayNameUtils = GrDisplayNameUtils;
   window.GrAnnotation = GrAnnotation;
+  window.GrAttributeHelper = GrAttributeHelper;
+  window.GrDiffLine = GrDiffLine;
+  window.GrDiffGroup = GrDiffGroup;
+  window.GrDiffBuilder = GrDiffBuilder;
+  window.GrDiffBuilderSideBySide = GrDiffBuilderSideBySide;
+  window.GrDiffBuilderImage = GrDiffBuilderImage;
+  window.GrDiffBuilderUnified = GrDiffBuilderUnified;
+  window.GrDiffBuilderBinary = GrDiffBuilderBinary;
+  window.GrChangeActionsInterface = GrChangeActionsInterface;
+  window.GrChangeReplyInterface = GrChangeReplyInterface;
+  window.GrEditConstants = GrEditConstants;
+  window.GrFileListConstants = GrFileListConstants;
+  window.GrDomHooksManager = GrDomHooksManager;
+  window.GrDomHook = GrDomHook;
+  window.GrEtagDecorator = GrEtagDecorator;
+  window.GrThemeApi = GrThemeApi;
+  window.SiteBasedCache = SiteBasedCache;
+  window.FetchPromisesCache = FetchPromisesCache;
+  window.GrRestApiHelper = GrRestApiHelper;
+  window.GrLinkTextParser = GrLinkTextParser;
+  window.GrPluginEndpoints = GrPluginEndpoints;
+  window.GrReviewerUpdatesParser = GrReviewerUpdatesParser;
+  window.GrPopupInterface = GrPopupInterface;
+  window.GrRangeNormalizer = GrRangeNormalizer;
+  window.GrCountStringFormatter = GrCountStringFormatter;
+  window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
 }
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
index 09620ef..d5ebb65 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -24,91 +24,85 @@
 
 document.head.appendChild($_documentContainer.content);
 
-(function(window) {
-  'use strict';
+/** @constructor */
+export function GrAttributeHelper(element) {
+  this.element = element;
+  this._promises = {};
+}
 
-  /** @constructor */
-  function GrAttributeHelper(element) {
-    this.element = element;
-    this._promises = {};
+GrAttributeHelper.prototype._getChangedEventName = function(name) {
+  return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + '-changed';
+};
+
+/**
+ * Returns true if the property is defined on wrapped element.
+ *
+ * @param {string} name
+ * @return {boolean}
+ */
+GrAttributeHelper.prototype._elementHasProperty = function(name) {
+  return this.element[name] !== undefined;
+};
+
+GrAttributeHelper.prototype._reportValue = function(callback, value) {
+  try {
+    callback(value);
+  } catch (e) {
+    console.info(e);
   }
+};
 
-  GrAttributeHelper.prototype._getChangedEventName = function(name) {
-    return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + '-changed';
-  };
+/**
+ * Binds callback to property updates.
+ *
+ * @param {string} name Property name.
+ * @param {function(?)} callback
+ * @return {function()} Unbind function.
+ */
+GrAttributeHelper.prototype.bind = function(name, callback) {
+  const attributeChangedEventName = this._getChangedEventName(name);
+  const changedHandler = e => this._reportValue(callback, e.detail.value);
+  const unbind = () => this.element.removeEventListener(
+      attributeChangedEventName, changedHandler);
+  this.element.addEventListener(
+      attributeChangedEventName, changedHandler);
+  if (this._elementHasProperty(name)) {
+    this._reportValue(callback, this.element[name]);
+  }
+  return unbind;
+};
 
-  /**
-   * Returns true if the property is defined on wrapped element.
-   *
-   * @param {string} name
-   * @return {boolean}
-   */
-  GrAttributeHelper.prototype._elementHasProperty = function(name) {
-    return this.element[name] !== undefined;
-  };
+/**
+ * Get value of the property from wrapped object. Waits for the property
+ * to be initialized if it isn't defined.
+ *
+ * @param {string} name Property name.
+ * @return {!Promise<?>}
+ */
+GrAttributeHelper.prototype.get = function(name) {
+  if (this._elementHasProperty(name)) {
+    return Promise.resolve(this.element[name]);
+  }
+  if (!this._promises[name]) {
+    let resolve;
+    const promise = new Promise(r => resolve = r);
+    const unbind = this.bind(name, value => {
+      resolve(value);
+      unbind();
+    });
+    this._promises[name] = promise;
+  }
+  return this._promises[name];
+};
 
-  GrAttributeHelper.prototype._reportValue = function(callback, value) {
-    try {
-      callback(value);
-    } catch (e) {
-      console.info(e);
-    }
-  };
-
-  /**
-   * Binds callback to property updates.
-   *
-   * @param {string} name Property name.
-   * @param {function(?)} callback
-   * @return {function()} Unbind function.
-   */
-  GrAttributeHelper.prototype.bind = function(name, callback) {
-    const attributeChangedEventName = this._getChangedEventName(name);
-    const changedHandler = e => this._reportValue(callback, e.detail.value);
-    const unbind = () => this.element.removeEventListener(
-        attributeChangedEventName, changedHandler);
-    this.element.addEventListener(
-        attributeChangedEventName, changedHandler);
-    if (this._elementHasProperty(name)) {
-      this._reportValue(callback, this.element[name]);
-    }
-    return unbind;
-  };
-
-  /**
-   * Get value of the property from wrapped object. Waits for the property
-   * to be initialized if it isn't defined.
-   *
-   * @param {string} name Property name.
-   * @return {!Promise<?>}
-   */
-  GrAttributeHelper.prototype.get = function(name) {
-    if (this._elementHasProperty(name)) {
-      return Promise.resolve(this.element[name]);
-    }
-    if (!this._promises[name]) {
-      let resolve;
-      const promise = new Promise(r => resolve = r);
-      const unbind = this.bind(name, value => {
-        resolve(value);
-        unbind();
-      });
-      this._promises[name] = promise;
-    }
-    return this._promises[name];
-  };
-
-  /**
-   * Sets value and dispatches event to force notify.
-   *
-   * @param {string} name Property name.
-   * @param {?} value
-   */
-  GrAttributeHelper.prototype.set = function(name, value) {
-    this.element[name] = value;
-    this.element.dispatchEvent(
-        new CustomEvent(this._getChangedEventName(name), {detail: {value}}));
-  };
-
-  window.GrAttributeHelper = GrAttributeHelper;
-})(window);
+/**
+ * Sets value and dispatches event to force notify.
+ *
+ * @param {string} name Property name.
+ * @param {?} value
+ */
+GrAttributeHelper.prototype.set = function(name, value) {
+  this.element[name] = value;
+  this.element.dispatchEvent(
+      new CustomEvent(this._getChangedEventName(name), {detail: {value}}));
+};
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
index fd2fd5b..50f9002 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -27,7 +27,6 @@
 <dom-element id="some-element">
   <script type="module">
 import '../../../test/common-test-setup.js';
-import './gr-attribute-helper.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 Polymer({
   is: 'some-element',
@@ -49,8 +48,8 @@
 </test-fixture>
 
 <script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-attribute-helper.js';
+import {GrAttributeHelper} from './gr-attribute-helper.js';
+
 suite('gr-attribute-helper tests', () => {
   let element;
   let instance;
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 9497493..5ef9dbf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -25,146 +25,139 @@
 
 document.head.appendChild($_documentContainer.content);
 
-(function(window) {
-  'use strict';
+/** @constructor */
+export function GrDomHooksManager(plugin) {
+  this._plugin = plugin;
+  this._hooks = {};
+}
 
-  /** @constructor */
-  function GrDomHooksManager(plugin) {
-    this._plugin = plugin;
-    this._hooks = {};
+GrDomHooksManager.prototype._getHookName = function(endpointName,
+    opt_moduleName) {
+  if (opt_moduleName) {
+    return endpointName + ' ' + opt_moduleName;
+  } else {
+    return this._plugin.getPluginName() + '-autogenerated-' + endpointName;
   }
+};
 
-  GrDomHooksManager.prototype._getHookName = function(endpointName,
-      opt_moduleName) {
-    if (opt_moduleName) {
-      return endpointName + ' ' + opt_moduleName;
-    } else {
-      return this._plugin.getPluginName() + '-autogenerated-' + endpointName;
-    }
-  };
-
-  GrDomHooksManager.prototype.getDomHook = function(endpointName,
-      opt_moduleName) {
-    const hookName = this._getHookName(endpointName, opt_moduleName);
-    if (!this._hooks[hookName]) {
-      this._hooks[hookName] = new GrDomHook(hookName, opt_moduleName);
-    }
-    return this._hooks[hookName];
-  };
-
-  /** @constructor */
-  function GrDomHook(hookName, opt_moduleName) {
-    this._instances = [];
-    this._attachCallbacks = [];
-    this._detachCallbacks = [];
-    if (opt_moduleName) {
-      this._moduleName = opt_moduleName;
-    } else {
-      this._moduleName = hookName;
-      this._createPlaceholder(hookName);
-    }
+GrDomHooksManager.prototype.getDomHook = function(endpointName,
+    opt_moduleName) {
+  const hookName = this._getHookName(endpointName, opt_moduleName);
+  if (!this._hooks[hookName]) {
+    this._hooks[hookName] = new GrDomHook(hookName, opt_moduleName);
   }
+  return this._hooks[hookName];
+};
 
-  GrDomHook.prototype._createPlaceholder = function(hookName) {
-    Polymer({
-      is: hookName,
-      properties: {
-        plugin: Object,
-        content: Object,
-      },
+/** @constructor */
+export function GrDomHook(hookName, opt_moduleName) {
+  this._instances = [];
+  this._attachCallbacks = [];
+  this._detachCallbacks = [];
+  if (opt_moduleName) {
+    this._moduleName = opt_moduleName;
+  } else {
+    this._moduleName = hookName;
+    this._createPlaceholder(hookName);
+  }
+}
+
+GrDomHook.prototype._createPlaceholder = function(hookName) {
+  Polymer({
+    is: hookName,
+    properties: {
+      plugin: Object,
+      content: Object,
+    },
+  });
+};
+
+GrDomHook.prototype.handleInstanceDetached = function(instance) {
+  const index = this._instances.indexOf(instance);
+  if (index !== -1) {
+    this._instances.splice(index, 1);
+  }
+  this._detachCallbacks.forEach(callback => callback(instance));
+};
+
+GrDomHook.prototype.handleInstanceAttached = function(instance) {
+  this._instances.push(instance);
+  this._attachCallbacks.forEach(callback => callback(instance));
+};
+
+/**
+ * Get instance of last DOM hook element attached into the endpoint.
+ * Returns a Promise, that's resolved when attachment is done.
+ *
+ * @return {!Promise<!Element>}
+ */
+GrDomHook.prototype.getLastAttached = function() {
+  if (this._instances.length) {
+    return Promise.resolve(this._instances.slice(-1)[0]);
+  }
+  if (!this._lastAttachedPromise) {
+    let resolve;
+    const promise = new Promise(r => resolve = r);
+    this._attachCallbacks.push(resolve);
+    this._lastAttachedPromise = promise.then(element => {
+      this._lastAttachedPromise = null;
+      const index = this._attachCallbacks.indexOf(resolve);
+      if (index !== -1) {
+        this._attachCallbacks.splice(index, 1);
+      }
+      return element;
     });
-  };
+  }
+  return this._lastAttachedPromise;
+};
 
-  GrDomHook.prototype.handleInstanceDetached = function(instance) {
-    const index = this._instances.indexOf(instance);
-    if (index !== -1) {
-      this._instances.splice(index, 1);
-    }
-    this._detachCallbacks.forEach(callback => callback(instance));
-  };
+/**
+ * Get all DOM hook elements.
+ */
+GrDomHook.prototype.getAllAttached = function() {
+  return this._instances;
+};
 
-  GrDomHook.prototype.handleInstanceAttached = function(instance) {
-    this._instances.push(instance);
-    this._attachCallbacks.forEach(callback => callback(instance));
-  };
+/**
+ * Install a new callback to invoke when a new instance of DOM hook element
+ * is attached.
+ *
+ * @param {function(Element)} callback
+ */
+GrDomHook.prototype.onAttached = function(callback) {
+  this._attachCallbacks.push(callback);
+  return this;
+};
 
-  /**
-   * Get instance of last DOM hook element attached into the endpoint.
-   * Returns a Promise, that's resolved when attachment is done.
-   *
-   * @return {!Promise<!Element>}
-   */
-  GrDomHook.prototype.getLastAttached = function() {
-    if (this._instances.length) {
-      return Promise.resolve(this._instances.slice(-1)[0]);
-    }
-    if (!this._lastAttachedPromise) {
-      let resolve;
-      const promise = new Promise(r => resolve = r);
-      this._attachCallbacks.push(resolve);
-      this._lastAttachedPromise = promise.then(element => {
-        this._lastAttachedPromise = null;
-        const index = this._attachCallbacks.indexOf(resolve);
-        if (index !== -1) {
-          this._attachCallbacks.splice(index, 1);
-        }
-        return element;
-      });
-    }
-    return this._lastAttachedPromise;
-  };
+/**
+ * Install a new callback to invoke when an instance of DOM hook element
+ * is detached.
+ *
+ * @param {function(Element)} callback
+ */
+GrDomHook.prototype.onDetached = function(callback) {
+  this._detachCallbacks.push(callback);
+  return this;
+};
 
-  /**
-   * Get all DOM hook elements.
-   */
-  GrDomHook.prototype.getAllAttached = function() {
-    return this._instances;
-  };
+/**
+ * Name of DOM hook element that will be installed into the endpoint.
+ */
+GrDomHook.prototype.getModuleName = function() {
+  return this._moduleName;
+};
 
-  /**
-   * Install a new callback to invoke when a new instance of DOM hook element
-   * is attached.
-   *
-   * @param {function(Element)} callback
-   */
-  GrDomHook.prototype.onAttached = function(callback) {
-    this._attachCallbacks.push(callback);
-    return this;
-  };
-
-  /**
-   * Install a new callback to invoke when an instance of DOM hook element
-   * is detached.
-   *
-   * @param {function(Element)} callback
-   */
-  GrDomHook.prototype.onDetached = function(callback) {
-    this._detachCallbacks.push(callback);
-    return this;
-  };
-
-  /**
-   * Name of DOM hook element that will be installed into the endpoint.
-   */
-  GrDomHook.prototype.getModuleName = function() {
-    return this._moduleName;
-  };
-
-  GrDomHook.prototype.getPublicAPI = function() {
-    const result = {};
-    const exposedMethods = [
-      'onAttached',
-      'onDetached',
-      'getLastAttached',
-      'getAllAttached',
-      'getModuleName',
-    ];
-    for (const p of exposedMethods) {
-      result[p] = this[p].bind(this);
-    }
-    return result;
-  };
-
-  window.GrDomHook = GrDomHook;
-  window.GrDomHooksManager = GrDomHooksManager;
-})(window);
+GrDomHook.prototype.getPublicAPI = function() {
+  const result = {};
+  const exposedMethods = [
+    'onAttached',
+    'onDetached',
+    'getLastAttached',
+    'getAllAttached',
+    'getModuleName',
+  ];
+  for (const p of exposedMethods) {
+    result[p] = this[p].bind(this);
+  }
+  return result;
+};
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
index 70d7a7d..43affdf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -32,8 +32,9 @@
 
 <script type="module">
 import '../../../test/common-test-setup.js';
-import './gr-dom-hooks.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {GrDomHook, GrDomHooksManager} from './gr-dom-hooks.js';
+
 suite('gr-dom-hooks tests', () => {
   const PUBLIC_METHODS =[
     'onAttached',
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
index e9d3e36..3363d72 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -26,64 +26,58 @@
 
 document.head.appendChild($_documentContainer.content);
 
-(function(window) {
-  'use strict';
+/**
+ * Plugin popup API.
+ * Provides method for opening and closing popups from plugin.
+ * opt_moduleName is a name of custom element that will be automatically
+ * inserted on popup opening.
+ *
+ * @constructor
+ * @param {!Object} plugin
+ * @param {opt_moduleName=} string
+ */
+export function GrPopupInterface(plugin, opt_moduleName) {
+  this.plugin = plugin;
+  this._openingPromise = null;
+  this._popup = null;
+  this._moduleName = opt_moduleName || null;
+}
 
-  /**
-   * Plugin popup API.
-   * Provides method for opening and closing popups from plugin.
-   * opt_moduleName is a name of custom element that will be automatically
-   * inserted on popup opening.
-   *
-   * @constructor
-   * @param {!Object} plugin
-   * @param {opt_moduleName=} string
-   */
-  function GrPopupInterface(plugin, opt_moduleName) {
-    this.plugin = plugin;
-    this._openingPromise = null;
-    this._popup = null;
-    this._moduleName = opt_moduleName || null;
+GrPopupInterface.prototype._getElement = function() {
+  return dom(this._popup);
+};
+
+/**
+ * Opens the popup, inserts it into DOM over current UI.
+ * Creates the popup if not previously created. Creates popup content element,
+ * if it was provided with constructor.
+ *
+ * @returns {!Promise<!Object>}
+ */
+GrPopupInterface.prototype.open = function() {
+  if (!this._openingPromise) {
+    this._openingPromise =
+        this.plugin.hook('plugin-overlay').getLastAttached()
+            .then(hookEl => {
+              const popup = document.createElement('gr-plugin-popup');
+              if (this._moduleName) {
+                const el = dom(popup).appendChild(
+                    document.createElement(this._moduleName));
+                el.plugin = this.plugin;
+              }
+              this._popup = dom(hookEl).appendChild(popup);
+              flush();
+              return this._popup.open().then(() => this);
+            });
   }
+  return this._openingPromise;
+};
 
-  GrPopupInterface.prototype._getElement = function() {
-    return dom(this._popup);
-  };
-
-  /**
-   * Opens the popup, inserts it into DOM over current UI.
-   * Creates the popup if not previously created. Creates popup content element,
-   * if it was provided with constructor.
-   *
-   * @returns {!Promise<!Object>}
-   */
-  GrPopupInterface.prototype.open = function() {
-    if (!this._openingPromise) {
-      this._openingPromise =
-          this.plugin.hook('plugin-overlay').getLastAttached()
-              .then(hookEl => {
-                const popup = document.createElement('gr-plugin-popup');
-                if (this._moduleName) {
-                  const el = dom(popup).appendChild(
-                      document.createElement(this._moduleName));
-                  el.plugin = this.plugin;
-                }
-                this._popup = dom(hookEl).appendChild(popup);
-                flush();
-                return this._popup.open().then(() => this);
-              });
-    }
-    return this._openingPromise;
-  };
-
-  /**
-   * Hides the popup.
-   */
-  GrPopupInterface.prototype.close = function() {
-    if (!this._popup) { return; }
-    this._popup.close();
-    this._openingPromise = null;
-  };
-
-  window.GrPopupInterface = GrPopupInterface;
-})(window);
+/**
+ * Hides the popup.
+ */
+GrPopupInterface.prototype.close = function() {
+  if (!this._popup) { return; }
+  this._popup.close();
+  this._openingPromise = null;
+};
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
index 661058e..f0896f8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -36,7 +36,6 @@
   </template>
   <script type="module">
 import '../../../test/common-test-setup.js';
-import './gr-popup-interface.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 Polymer({is: 'gr-user-test-popup'});
@@ -45,9 +44,10 @@
 
 <script type="module">
 import '../../../test/common-test-setup.js';
-import './gr-popup-interface.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GrPopupInterface} from './gr-popup-interface.js';
+
 suite('gr-popup-interface tests', () => {
   let container;
   let instance;
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index 8da680b..c987af3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -25,27 +25,19 @@
 
 document.head.appendChild($_documentContainer.content);
 
-(function(window) {
-  'use strict';
+/** @constructor */
+export function GrThemeApi(plugin) {
+  this.plugin = plugin;
+}
 
-  // Prevent redefinition.
-  if (window.GrThemeApi) { return; }
+GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) {
+  this.plugin.hook('header-title', {replace: true}).onAttached(
+      element => {
+        const customHeader =
+              document.createElement('gr-custom-plugin-header');
+        customHeader.logoUrl = logoUrl;
+        customHeader.title = title;
+        element.appendChild(customHeader);
+      });
+};
 
-  /** @constructor */
-  function GrThemeApi(plugin) {
-    this.plugin = plugin;
-  }
-
-  GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) {
-    this.plugin.hook('header-title', {replace: true}).onAttached(
-        element => {
-          const customHeader =
-                document.createElement('gr-custom-plugin-header');
-          customHeader.logoUrl = logoUrl;
-          customHeader.title = title;
-          element.appendChild(customHeader);
-        });
-  };
-
-  window.GrThemeApi = GrThemeApi;
-})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
index 83ea1bd..7c524dd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -35,7 +35,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
-import './gr-theme-api.js';
+
 suite('gr-theme-api tests', () => {
   let sandbox;
   let theme;
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
index 02c57e0..1c3a689 100644
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
-  const GrCountStringFormatter = window.GrCountStringFormatter || {};
-
+export const GrCountStringFormatter = {
   /**
    * Returns a count plus string that is pluralized when necessary.
    *
@@ -25,9 +22,9 @@
    * @param {string} noun
    * @return {string}
    */
-  GrCountStringFormatter.computePluralString = function(count, noun) {
+  computePluralString(count, noun) {
     return this.computeString(count, noun) + (count > 1 ? 's' : '');
-  };
+  },
 
   /**
    * Returns a count plus string that is not pluralized.
@@ -36,10 +33,12 @@
    * @param {string} noun
    * @return {string}
    */
-  GrCountStringFormatter.computeString = function(count, noun) {
-    if (count === 0) { return ''; }
+  computeString(count, noun) {
+    if (count === 0) {
+      return '';
+    }
     return count + ' ' + noun;
-  };
+  },
 
   /**
    * Returns a count plus arbitrary text.
@@ -48,9 +47,10 @@
    * @param {string} text
    * @return {string}
    */
-  GrCountStringFormatter.computeShortString = function(count, text) {
-    if (count === 0) { return ''; }
+  computeShortString(count, text) {
+    if (count === 0) {
+      return '';
+    }
     return count + text;
-  };
-  window.GrCountStringFormatter = GrCountStringFormatter;
-})(window);
+  },
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
index ead0191..7f08603 100644
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
@@ -25,7 +25,8 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script type="module">
 import '../../../test/common-test-setup.js';
-import './gr-count-string-formatter.js';
+import {GrCountStringFormatter} from './gr-count-string-formatter.js';
+
 suite('gr-count-string-formatter tests', () => {
   test('computeString', () => {
     const noun = 'unresolved';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
index 7c4c817..8ab97f8 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -14,127 +14,122 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  /**
-   * Ensure GrChangeActionsInterface instance has access to gr-change-actions
-   * element and retrieve if the interface was created before element.
-   *
-   * @param {!GrChangeActionsInterface} api
-   */
-  function ensureEl(api) {
-    if (!api._el) {
-      const sharedApiElement = document.createElement('gr-js-api-interface');
-      setEl(api, sharedApiElement.getElement(
-          sharedApiElement.Element.CHANGE_ACTIONS));
-    }
+/**
+ * Ensure GrChangeActionsInterface instance has access to gr-change-actions
+ * element and retrieve if the interface was created before element.
+ *
+ * @param {!GrChangeActionsInterface} api
+ */
+function ensureEl(api) {
+  if (!api._el) {
+    const sharedApiElement = document.createElement('gr-js-api-interface');
+    setEl(api, sharedApiElement.getElement(
+        sharedApiElement.Element.CHANGE_ACTIONS));
   }
+}
 
-  /**
-   * Set gr-change-actions element to a GrChangeActionsInterface instance.
-   *
-   * @param {!GrChangeActionsInterface} api
-   * @param {!Element} el gr-change-actions
-   */
-  function setEl(api, el) {
-    if (!el) {
-      console.warn('changeActions() is not ready');
-      return;
-    }
-    api._el = el;
-    api.RevisionActions = el.RevisionActions;
-    api.ChangeActions = el.ChangeActions;
-    api.ActionType = el.ActionType;
+/**
+ * Set gr-change-actions element to a GrChangeActionsInterface instance.
+ *
+ * @param {!GrChangeActionsInterface} api
+ * @param {!Element} el gr-change-actions
+ */
+function setEl(api, el) {
+  if (!el) {
+    console.warn('changeActions() is not ready');
+    return;
   }
+  api._el = el;
+  api.RevisionActions = el.RevisionActions;
+  api.ChangeActions = el.ChangeActions;
+  api.ActionType = el.ActionType;
+}
 
-  function GrChangeActionsInterface(plugin, el) {
-    this.plugin = plugin;
-    setEl(this, el);
-  }
+export function GrChangeActionsInterface(plugin, el) {
+  this.plugin = plugin;
+  setEl(this, el);
+}
 
-  GrChangeActionsInterface.prototype.addPrimaryActionKey = function(key) {
-    ensureEl(this);
-    if (this._el.primaryActionKeys.includes(key)) { return; }
+GrChangeActionsInterface.prototype.addPrimaryActionKey = function(key) {
+  ensureEl(this);
+  if (this._el.primaryActionKeys.includes(key)) { return; }
 
-    this._el.push('primaryActionKeys', key);
-  };
+  this._el.push('primaryActionKeys', key);
+};
 
-  GrChangeActionsInterface.prototype.removePrimaryActionKey = function(key) {
-    ensureEl(this);
-    this._el.primaryActionKeys = this._el.primaryActionKeys
-        .filter(k => k !== key);
-  };
+GrChangeActionsInterface.prototype.removePrimaryActionKey = function(key) {
+  ensureEl(this);
+  this._el.primaryActionKeys = this._el.primaryActionKeys
+      .filter(k => k !== key);
+};
 
-  GrChangeActionsInterface.prototype.hideQuickApproveAction = function() {
-    ensureEl(this);
-    this._el.hideQuickApproveAction();
-  };
+GrChangeActionsInterface.prototype.hideQuickApproveAction = function() {
+  ensureEl(this);
+  this._el.hideQuickApproveAction();
+};
 
-  GrChangeActionsInterface.prototype.setActionOverflow = function(type, key,
-      overflow) {
-    ensureEl(this);
-    return this._el.setActionOverflow(type, key, overflow);
-  };
+GrChangeActionsInterface.prototype.setActionOverflow = function(type, key,
+    overflow) {
+  ensureEl(this);
+  return this._el.setActionOverflow(type, key, overflow);
+};
 
-  GrChangeActionsInterface.prototype.setActionPriority = function(type, key,
-      priority) {
-    ensureEl(this);
-    return this._el.setActionPriority(type, key, priority);
-  };
+GrChangeActionsInterface.prototype.setActionPriority = function(type, key,
+    priority) {
+  ensureEl(this);
+  return this._el.setActionPriority(type, key, priority);
+};
 
-  GrChangeActionsInterface.prototype.setActionHidden = function(type, key,
-      hidden) {
-    ensureEl(this);
-    return this._el.setActionHidden(type, key, hidden);
-  };
+GrChangeActionsInterface.prototype.setActionHidden = function(type, key,
+    hidden) {
+  ensureEl(this);
+  return this._el.setActionHidden(type, key, hidden);
+};
 
-  GrChangeActionsInterface.prototype.add = function(type, label) {
-    ensureEl(this);
-    return this._el.addActionButton(type, label);
-  };
+GrChangeActionsInterface.prototype.add = function(type, label) {
+  ensureEl(this);
+  return this._el.addActionButton(type, label);
+};
 
-  GrChangeActionsInterface.prototype.remove = function(key) {
-    ensureEl(this);
-    return this._el.removeActionButton(key);
-  };
+GrChangeActionsInterface.prototype.remove = function(key) {
+  ensureEl(this);
+  return this._el.removeActionButton(key);
+};
 
-  GrChangeActionsInterface.prototype.addTapListener = function(key, handler) {
-    ensureEl(this);
-    this._el.addEventListener(key + '-tap', handler);
-  };
+GrChangeActionsInterface.prototype.addTapListener = function(key, handler) {
+  ensureEl(this);
+  this._el.addEventListener(key + '-tap', handler);
+};
 
-  GrChangeActionsInterface.prototype.removeTapListener = function(key,
-      handler) {
-    ensureEl(this);
-    this._el.removeEventListener(key + '-tap', handler);
-  };
+GrChangeActionsInterface.prototype.removeTapListener = function(key,
+    handler) {
+  ensureEl(this);
+  this._el.removeEventListener(key + '-tap', handler);
+};
 
-  GrChangeActionsInterface.prototype.setLabel = function(key, text) {
-    ensureEl(this);
-    this._el.setActionButtonProp(key, 'label', text);
-  };
+GrChangeActionsInterface.prototype.setLabel = function(key, text) {
+  ensureEl(this);
+  this._el.setActionButtonProp(key, 'label', text);
+};
 
-  GrChangeActionsInterface.prototype.setTitle = function(key, text) {
-    ensureEl(this);
-    this._el.setActionButtonProp(key, 'title', text);
-  };
+GrChangeActionsInterface.prototype.setTitle = function(key, text) {
+  ensureEl(this);
+  this._el.setActionButtonProp(key, 'title', text);
+};
 
-  GrChangeActionsInterface.prototype.setEnabled = function(key, enabled) {
-    ensureEl(this);
-    this._el.setActionButtonProp(key, 'enabled', enabled);
-  };
+GrChangeActionsInterface.prototype.setEnabled = function(key, enabled) {
+  ensureEl(this);
+  this._el.setActionButtonProp(key, 'enabled', enabled);
+};
 
-  GrChangeActionsInterface.prototype.setIcon = function(key, icon) {
-    ensureEl(this);
-    this._el.setActionButtonProp(key, 'icon', icon);
-  };
+GrChangeActionsInterface.prototype.setIcon = function(key, icon) {
+  ensureEl(this);
+  this._el.setActionButtonProp(key, 'icon', icon);
+};
 
-  GrChangeActionsInterface.prototype.getActionDetails = function(action) {
-    ensureEl(this);
-    return this._el.getActionDetails(action) ||
-      this._el.getActionDetails(this.plugin.getPluginName() + '~' + action);
-  };
-
-  window.GrChangeActionsInterface = GrChangeActionsInterface;
-})(window);
+GrChangeActionsInterface.prototype.getActionDetails = function(action) {
+  ensureEl(this);
+  return this._el.getActionDetails(action) ||
+    this._el.getActionDetails(this.plugin.getPluginName() + '~' + action);
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
index d74f99e..da0157f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -14,66 +14,61 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  /**
-   * GrChangeReplyInterface, provides a set of handy methods on reply dialog.
-   */
-  class GrChangeReplyInterface {
-    constructor(plugin) {
-      this.plugin = plugin;
-      this._sharedApiEl = Plugin._sharedAPIElement;
-    }
-
-    get _el() {
-      return this._sharedApiEl.getElement(
-          this._sharedApiEl.Element.REPLY_DIALOG);
-    }
-
-    getLabelValue(label) {
-      return this._el.getLabelValue(label);
-    }
-
-    setLabelValue(label, value) {
-      this._el.setLabelValue(label, value);
-    }
-
-    send(opt_includeComments) {
-      this._el.send(opt_includeComments);
-    }
-
-    addReplyTextChangedCallback(handler) {
-      const hookApi = this.plugin.hook('reply-text');
-      const registeredHandler = e => handler(e.detail.value);
-      hookApi.onAttached(el => {
-        if (!el.content) { return; }
-        el.content.addEventListener('value-changed', registeredHandler);
-      });
-      hookApi.onDetached(el => {
-        if (!el.content) { return; }
-        el.content.removeEventListener('value-changed', registeredHandler);
-      });
-    }
-
-    addLabelValuesChangedCallback(handler) {
-      const hookApi = this.plugin.hook('reply-label-scores');
-      const registeredHandler = e => handler(e.detail);
-      hookApi.onAttached(el => {
-        if (!el.content) { return; }
-        el.content.addEventListener('labels-changed', registeredHandler);
-      });
-
-      hookApi.onDetached(el => {
-        if (!el.content) { return; }
-        el.content.removeEventListener('labels-changed', registeredHandler);
-      });
-    }
-
-    showMessage(message) {
-      return this._el.setPluginMessage(message);
-    }
+/**
+ * GrChangeReplyInterface, provides a set of handy methods on reply dialog.
+ */
+export class GrChangeReplyInterface {
+  constructor(plugin) {
+    this.plugin = plugin;
+    this._sharedApiEl = Plugin._sharedAPIElement;
   }
 
-  window.GrChangeReplyInterface = GrChangeReplyInterface;
-})(window);
+  get _el() {
+    return this._sharedApiEl.getElement(
+        this._sharedApiEl.Element.REPLY_DIALOG);
+  }
+
+  getLabelValue(label) {
+    return this._el.getLabelValue(label);
+  }
+
+  setLabelValue(label, value) {
+    this._el.setLabelValue(label, value);
+  }
+
+  send(opt_includeComments) {
+    this._el.send(opt_includeComments);
+  }
+
+  addReplyTextChangedCallback(handler) {
+    const hookApi = this.plugin.hook('reply-text');
+    const registeredHandler = e => handler(e.detail.value);
+    hookApi.onAttached(el => {
+      if (!el.content) { return; }
+      el.content.addEventListener('value-changed', registeredHandler);
+    });
+    hookApi.onDetached(el => {
+      if (!el.content) { return; }
+      el.content.removeEventListener('value-changed', registeredHandler);
+    });
+  }
+
+  addLabelValuesChangedCallback(handler) {
+    const hookApi = this.plugin.hook('reply-label-scores');
+    const registeredHandler = e => handler(e.detail);
+    hookApi.onAttached(el => {
+      if (!el.content) { return; }
+      el.content.addEventListener('labels-changed', registeredHandler);
+    });
+
+    hookApi.onDetached(el => {
+      if (!el.content) { return; }
+      el.content.removeEventListener('labels-changed', registeredHandler);
+    });
+  }
+
+  showMessage(message) {
+    return this._el.setPluginMessage(message);
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
index b0fe09e..926a1b1 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
@@ -20,6 +20,8 @@
  * should be defined or linked here.
  */
 
+import {GrPluginEndpoints} from './gr-plugin-endpoints.js';
+
 (function(window) {
   'use strict';
 
@@ -194,4 +196,4 @@
      */
     Gerrit[method] = Gerrit._eventEmitter[method].bind(Gerrit._eventEmitter);
   });
-})(window);
\ No newline at end of file
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index f5ffad8..28a168b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -17,24 +17,17 @@
 import '../../../scripts/bundled-polymer.js';
 import '../../core/gr-reporting/gr-reporting.js';
 import '../../plugins/gr-admin-api/gr-admin-api.js';
-import '../../plugins/gr-attribute-helper/gr-attribute-helper.js';
 import '../../plugins/gr-change-metadata-api/gr-change-metadata-api.js';
-import '../../plugins/gr-dom-hooks/gr-dom-hooks.js';
 import '../../plugins/gr-event-helper/gr-event-helper.js';
-import '../../plugins/gr-popup-interface/gr-popup-interface.js';
 import '../../plugins/gr-repo-api/gr-repo-api.js';
 import '../../plugins/gr-settings-api/gr-settings-api.js';
 import '../../plugins/gr-styles-api/gr-styles-api.js';
-import '../../plugins/gr-theme-api/gr-theme-api.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import './gr-api-utils.js';
 import '../gr-event-interface/gr-event-interface.js';
 import './gr-annotation-actions-context.js';
 import './gr-annotation-actions-js-api.js';
-import './gr-change-actions-js-api.js';
-import './gr-change-reply-js-api.js';
 import './gr-js-api-interface-element.js';
-import './gr-plugin-endpoints.js';
 import './gr-plugin-action-context.js';
 import './gr-plugin-rest-api.js';
 import './gr-public-js-api.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 9a2e688..f28271e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -34,6 +34,7 @@
 import '../../../test/common-test-setup.js';
 import './gr-js-api-interface.js';
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface.js';
 
 suite('gr-js-api-interface tests', () => {
   const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
@@ -402,13 +403,17 @@
     });
 
     test('popup(moduleName) creates popup with component', () => {
-      const openStub = sandbox.stub();
-      sandbox.stub(window, 'GrPopupInterface').returns({
-        open: openStub,
-      });
+      const openStub = sandbox.stub(GrPopupInterface.prototype, 'open',
+          function() {
+            // Arrow function can't be used here, because we want to
+            // get properties from the instance of GrPopupInterface
+            // eslint-disable-next-line no-invalid-this
+            const grPopupInterface = this;
+            assert.equal(grPopupInterface.plugin, plugin);
+            assert.equal(grPopupInterface._moduleName, 'some-name');
+          });
       plugin.popup('some-name');
       assert.isTrue(openStub.calledOnce);
-      assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name'));
     });
 
     test('deprecated.popup(element) creates popup with element', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
index 42d9056..a838c4d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
@@ -14,149 +14,144 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  /** @constructor */
-  function GrPluginEndpoints() {
-    this._endpoints = {};
-    this._callbacks = {};
-    this._dynamicPlugins = {};
+/** @constructor */
+export function GrPluginEndpoints() {
+  this._endpoints = {};
+  this._callbacks = {};
+  this._dynamicPlugins = {};
+}
+
+GrPluginEndpoints.prototype.onNewEndpoint = function(endpoint, callback) {
+  if (!this._callbacks[endpoint]) {
+    this._callbacks[endpoint] = [];
   }
+  this._callbacks[endpoint].push(callback);
+};
 
-  GrPluginEndpoints.prototype.onNewEndpoint = function(endpoint, callback) {
-    if (!this._callbacks[endpoint]) {
-      this._callbacks[endpoint] = [];
-    }
-    this._callbacks[endpoint].push(callback);
-  };
+GrPluginEndpoints.prototype.onDetachedEndpoint = function(endpoint,
+    callback) {
+  if (this._callbacks[endpoint]) {
+    this._callbacks[endpoint] = this._callbacks[endpoint]
+        .filter(cb => cb !== callback);
+  }
+};
 
-  GrPluginEndpoints.prototype.onDetachedEndpoint = function(endpoint,
-      callback) {
-    if (this._callbacks[endpoint]) {
-      this._callbacks[endpoint] = this._callbacks[endpoint]
-          .filter(cb => cb !== callback);
-    }
-  };
+GrPluginEndpoints.prototype._getOrCreateModuleInfo = function(plugin,
+    endpoint, type, moduleName, domHook) {
+  const existingModule = this._endpoints[endpoint].find(info =>
+    info.plugin === plugin &&
+      info.moduleName === moduleName &&
+      info.domHook === domHook
+  );
+  if (existingModule) {
+    return existingModule;
+  } else {
+    const newModule = {
+      moduleName,
+      plugin,
+      pluginUrl: plugin._url,
+      type,
+      domHook,
+    };
+    this._endpoints[endpoint].push(newModule);
+    return newModule;
+  }
+};
 
-  GrPluginEndpoints.prototype._getOrCreateModuleInfo = function(plugin,
-      endpoint, type, moduleName, domHook) {
-    const existingModule = this._endpoints[endpoint].find(info =>
-      info.plugin === plugin &&
-        info.moduleName === moduleName &&
-        info.domHook === domHook
-    );
-    if (existingModule) {
-      return existingModule;
-    } else {
-      const newModule = {
-        moduleName,
-        plugin,
-        pluginUrl: plugin._url,
-        type,
-        domHook,
-      };
-      this._endpoints[endpoint].push(newModule);
-      return newModule;
+/**
+ * Register a plugin to an endpoint.
+ *
+ * Dynamic plugins are registered to a specific prefix, such as
+ * 'change-list-header'. These plugins are then fetched by prefix to determine
+ * which endpoints to dynamically add to the page.
+ */
+GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
+    moduleName, domHook, dynamicEndpoint) {
+  if (dynamicEndpoint) {
+    if (!this._dynamicPlugins[dynamicEndpoint]) {
+      this._dynamicPlugins[dynamicEndpoint] = new Set();
     }
-  };
+    this._dynamicPlugins[dynamicEndpoint].add(endpoint);
+  }
+  if (!this._endpoints[endpoint]) {
+    this._endpoints[endpoint] = [];
+  }
+  const moduleInfo = this._getOrCreateModuleInfo(plugin, endpoint, type,
+      moduleName, domHook);
+  if (Gerrit._arePluginsLoaded() && this._callbacks[endpoint]) {
+    this._callbacks[endpoint].forEach(callback => callback(moduleInfo));
+  }
+};
 
-  /**
-   * Register a plugin to an endpoint.
-   *
-   * Dynamic plugins are registered to a specific prefix, such as
-   * 'change-list-header'. These plugins are then fetched by prefix to determine
-   * which endpoints to dynamically add to the page.
-   */
-  GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
-      moduleName, domHook, dynamicEndpoint) {
-    if (dynamicEndpoint) {
-      if (!this._dynamicPlugins[dynamicEndpoint]) {
-        this._dynamicPlugins[dynamicEndpoint] = new Set();
-      }
-      this._dynamicPlugins[dynamicEndpoint].add(endpoint);
-    }
-    if (!this._endpoints[endpoint]) {
-      this._endpoints[endpoint] = [];
-    }
-    const moduleInfo = this._getOrCreateModuleInfo(plugin, endpoint, type,
-        moduleName, domHook);
-    if (Gerrit._arePluginsLoaded() && this._callbacks[endpoint]) {
-      this._callbacks[endpoint].forEach(callback => callback(moduleInfo));
-    }
-  };
+GrPluginEndpoints.prototype.getDynamicEndpoints = function(dynamicEndpoint) {
+  const plugins = this._dynamicPlugins[dynamicEndpoint];
+  if (!plugins) return [];
+  return Array.from(plugins);
+};
 
-  GrPluginEndpoints.prototype.getDynamicEndpoints = function(dynamicEndpoint) {
-    const plugins = this._dynamicPlugins[dynamicEndpoint];
-    if (!plugins) return [];
-    return Array.from(plugins);
-  };
+/**
+ * Get detailed information about modules registered with an extension
+ * endpoint.
+ *
+ * @param {string} name Endpoint name.
+ * @param {?{
+ *   type: (string|undefined),
+ *   moduleName: (string|undefined)
+ * }} opt_options
+ * @return {!Array<{
+ *   moduleName: string,
+ *   plugin: Plugin,
+ *   pluginUrl: String,
+ *   type: EndpointType,
+ *   domHook: !Object
+ * }>}
+ */
+GrPluginEndpoints.prototype.getDetails = function(name, opt_options) {
+  const type = opt_options && opt_options.type;
+  const moduleName = opt_options && opt_options.moduleName;
+  if (!this._endpoints[name]) {
+    return [];
+  }
+  return this._endpoints[name]
+      .filter(item => (!type || item.type === type) &&
+                  (!moduleName || moduleName == item.moduleName));
+};
 
-  /**
-   * Get detailed information about modules registered with an extension
-   * endpoint.
-   *
-   * @param {string} name Endpoint name.
-   * @param {?{
-   *   type: (string|undefined),
-   *   moduleName: (string|undefined)
-   * }} opt_options
-   * @return {!Array<{
-   *   moduleName: string,
-   *   plugin: Plugin,
-   *   pluginUrl: String,
-   *   type: EndpointType,
-   *   domHook: !Object
-   * }>}
-   */
-  GrPluginEndpoints.prototype.getDetails = function(name, opt_options) {
-    const type = opt_options && opt_options.type;
-    const moduleName = opt_options && opt_options.moduleName;
-    if (!this._endpoints[name]) {
-      return [];
-    }
-    return this._endpoints[name]
-        .filter(item => (!type || item.type === type) &&
-                    (!moduleName || moduleName == item.moduleName));
-  };
+/**
+ * Get detailed module names for instantiating at the endpoint.
+ *
+ * @param {string} name Endpoint name.
+ * @param {?{
+ *   type: (string|undefined),
+ *   moduleName: (string|undefined)
+ * }} opt_options
+ * @return {!Array<string>}
+ */
+GrPluginEndpoints.prototype.getModules = function(name, opt_options) {
+  const modulesData = this.getDetails(name, opt_options);
+  if (!modulesData.length) {
+    return [];
+  }
+  return modulesData.map(m => m.moduleName);
+};
 
-  /**
-   * Get detailed module names for instantiating at the endpoint.
-   *
-   * @param {string} name Endpoint name.
-   * @param {?{
-   *   type: (string|undefined),
-   *   moduleName: (string|undefined)
-   * }} opt_options
-   * @return {!Array<string>}
-   */
-  GrPluginEndpoints.prototype.getModules = function(name, opt_options) {
-    const modulesData = this.getDetails(name, opt_options);
-    if (!modulesData.length) {
-      return [];
-    }
-    return modulesData.map(m => m.moduleName);
-  };
-
-  /**
-   * Get .html plugin URLs with element and module definitions.
-   *
-   * @param {string} name Endpoint name.
-   * @param {?{
-   *   type: (string|undefined),
-   *   moduleName: (string|undefined)
-   * }} opt_options
-   * @return {!Array<!URL>}
-   */
-  GrPluginEndpoints.prototype.getPlugins = function(name, opt_options) {
-    const modulesData =
-          this.getDetails(name, opt_options).filter(
-              data => data.pluginUrl.pathname.includes('.html'));
-    if (!modulesData.length) {
-      return [];
-    }
-    return Array.from(new Set(modulesData.map(m => m.pluginUrl)));
-  };
-
-  window.GrPluginEndpoints = GrPluginEndpoints;
-})(window);
+/**
+ * Get .html plugin URLs with element and module definitions.
+ *
+ * @param {string} name Endpoint name.
+ * @param {?{
+ *   type: (string|undefined),
+ *   moduleName: (string|undefined)
+ * }} opt_options
+ * @return {!Array<!URL>}
+ */
+GrPluginEndpoints.prototype.getPlugins = function(name, opt_options) {
+  const modulesData =
+        this.getDetails(name, opt_options).filter(
+            data => data.pluginUrl.pathname.includes('.html'));
+  if (!modulesData.length) {
+    return [];
+  }
+  return Array.from(new Set(modulesData.map(m => m.pluginUrl)));
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index 39c3385..2194064 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -27,6 +27,8 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import './gr-js-api-interface.js';
+import {GrPluginEndpoints} from './gr-plugin-endpoints.js';
+
 suite('gr-plugin-endpoints tests', () => {
   let sandbox;
   let instance;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 3cf38e7..99cab99 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -16,6 +16,12 @@
  */
 
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {GrAttributeHelper} from '../../plugins/gr-attribute-helper/gr-attribute-helper.js';
+import {GrChangeActionsInterface} from './gr-change-actions-js-api.js';
+import {GrChangeReplyInterface} from './gr-change-reply-js-api.js';
+import {GrDomHooksManager} from '../../plugins/gr-dom-hooks/gr-dom-hooks.js';
+import {GrThemeApi} from '../../plugins/gr-theme-api/gr-theme-api.js';
+import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface.js';
 
 (function(window) {
   'use strict';
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index f8a48e6..7e55652 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -18,13 +18,13 @@
 
 import '../../../styles/shared-styles.js';
 import '../../core/gr-navigation/gr-navigation.js';
-import './link-text-parser.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import 'ba-linkify/ba-linkify.js';
 import {htmlTemplate} from './gr-linked-text_html.js';
+import {GrLinkTextParser} from './link-text-parser.js';
 
 /** @extends Polymer.Element */
 class GrLinkedText extends GestureEventListeners(
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index 185e394..6f8a88a 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -17,344 +17,340 @@
 
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
-(function() {
-  'use strict';
-  /**
-   * Pattern describing URLs with supported protocols.
-   *
-   * @type {RegExp}
-   */
-  const URL_PROTOCOL_PATTERN = /^(.*)(https?:\/\/|mailto:)/;
+/**
+ * Pattern describing URLs with supported protocols.
+ *
+ * @type {RegExp}
+ */
+const URL_PROTOCOL_PATTERN = /^(.*)(https?:\/\/|mailto:)/;
 
-  /**
-   * Construct a parser for linkifying text. Will linkify plain URLs that appear
-   * in the text as well as custom links if any are specified in the linkConfig
-   * parameter.
-   *
-   * @constructor
-   * @param {Object|null|undefined} linkConfig Comment links as specified by the
-   *     commentlinks field on a project config.
-   * @param {Function} callback The callback to be fired when an intermediate
-   *     parse result is emitted. The callback is passed text and href strings
-   *     if a link is to be created, or a document fragment otherwise.
-   * @param {boolean|undefined} opt_removeZeroWidthSpace If true, zero-width
-   *     spaces will be removed from R=<email> and CC=<email> expressions.
-   */
-  function GrLinkTextParser(linkConfig, callback, opt_removeZeroWidthSpace) {
-    this.linkConfig = linkConfig;
-    this.callback = callback;
-    this.removeZeroWidthSpace = opt_removeZeroWidthSpace;
-    this.baseUrl = BaseUrlBehavior.getBaseUrl();
-    Object.preventExtensions(this);
+/**
+ * Construct a parser for linkifying text. Will linkify plain URLs that appear
+ * in the text as well as custom links if any are specified in the linkConfig
+ * parameter.
+ *
+ * @constructor
+ * @param {Object|null|undefined} linkConfig Comment links as specified by the
+ *     commentlinks field on a project config.
+ * @param {Function} callback The callback to be fired when an intermediate
+ *     parse result is emitted. The callback is passed text and href strings
+ *     if a link is to be created, or a document fragment otherwise.
+ * @param {boolean|undefined} opt_removeZeroWidthSpace If true, zero-width
+ *     spaces will be removed from R=<email> and CC=<email> expressions.
+ */
+export function GrLinkTextParser(linkConfig, callback,
+    opt_removeZeroWidthSpace) {
+  this.linkConfig = linkConfig;
+  this.callback = callback;
+  this.removeZeroWidthSpace = opt_removeZeroWidthSpace;
+  this.baseUrl = BaseUrlBehavior.getBaseUrl();
+  Object.preventExtensions(this);
+}
+
+/**
+ * Emit a callback to create a link element.
+ *
+ * @param {string} text The text of the link.
+ * @param {string} href The URL to use as the href of the link.
+ */
+GrLinkTextParser.prototype.addText = function(text, href) {
+  if (!text) { return; }
+  this.callback(text, href);
+};
+
+/**
+ * Given the source text and a list of CommentLinkItem objects that were
+ * generated by the commentlinks config, emit parsing callbacks.
+ *
+ * @param {string} text The chuml of source text over which the outputArray
+ *     items range.
+ * @param {!Array<Gerrit.CommentLinkItem>} outputArray The list of items to add
+ *     resulting from commentlink matches.
+ */
+GrLinkTextParser.prototype.processLinks = function(text, outputArray) {
+  this.sortArrayReverse(outputArray);
+  const fragment = document.createDocumentFragment();
+  let cursor = text.length;
+
+  // Start inserting linkified URLs from the end of the String. That way, the
+  // string positions of the items don't change as we iterate through.
+  outputArray.forEach(item => {
+    // Add any text between the current linkified item and the item added
+    // before if it exists.
+    if (item.position + item.length !== cursor) {
+      fragment.insertBefore(
+          document.createTextNode(
+              text.slice(item.position + item.length, cursor)),
+          fragment.firstChild);
+    }
+    fragment.insertBefore(item.html, fragment.firstChild);
+    cursor = item.position;
+  });
+
+  // Add the beginning portion at the end.
+  if (cursor !== 0) {
+    fragment.insertBefore(
+        document.createTextNode(text.slice(0, cursor)), fragment.firstChild);
   }
 
-  /**
-   * Emit a callback to create a link element.
-   *
-   * @param {string} text The text of the link.
-   * @param {string} href The URL to use as the href of the link.
-   */
-  GrLinkTextParser.prototype.addText = function(text, href) {
-    if (!text) { return; }
-    this.callback(text, href);
-  };
+  this.callback(null, null, fragment);
+};
 
-  /**
-   * Given the source text and a list of CommentLinkItem objects that were
-   * generated by the commentlinks config, emit parsing callbacks.
-   *
-   * @param {string} text The chuml of source text over which the outputArray
-   *     items range.
-   * @param {!Array<Gerrit.CommentLinkItem>} outputArray The list of items to add
-   *     resulting from commentlink matches.
-   */
-  GrLinkTextParser.prototype.processLinks = function(text, outputArray) {
-    this.sortArrayReverse(outputArray);
-    const fragment = document.createDocumentFragment();
-    let cursor = text.length;
+/**
+ * Sort the given array of CommentLinkItems such that the positions are in
+ * reverse order.
+ *
+ * @param {!Array<Gerrit.CommentLinkItem>} outputArray
+ */
+GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) {
+  outputArray.sort((a, b) => b.position - a.position);
+};
 
-    // Start inserting linkified URLs from the end of the String. That way, the
-    // string positions of the items don't change as we iterate through.
-    outputArray.forEach(item => {
-      // Add any text between the current linkified item and the item added
-      // before if it exists.
-      if (item.position + item.length !== cursor) {
-        fragment.insertBefore(
-            document.createTextNode(
-                text.slice(item.position + item.length, cursor)),
-            fragment.firstChild);
+/**
+ * Create a CommentLinkItem and append it to the given output array. This
+ * method can be called in either of two ways:
+ * - With `text` and `href` parameters provided, and the `html` parameter
+ *   passed as `null`. In this case, the new CommentLinkItem will be a link
+ *   element with the given text and href value.
+ * - With the `html` paremeter provided, and the `text` and `href` parameters
+ *   passed as `null`. In this case, the string of HTML will be parsed and the
+ *   first resulting node will be used as the resulting content.
+ *
+ * @param {string|null} text The text to use if creating a link.
+ * @param {string|null} href The href to use as the URL if creating a link.
+ * @param {string|null} html The html to parse and use as the result.
+ * @param {number} position The position inside the source text where the item
+ *     starts.
+ * @param {number} length The number of characters in the source text
+ *     represented by the item.
+ * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
+ *     new item is to be appended.
+ */
+GrLinkTextParser.prototype.addItem =
+    function(text, href, html, position, length, outputArray) {
+      let htmlOutput = '';
+
+      if (href) {
+        const a = document.createElement('a');
+        a.href = href;
+        a.textContent = text;
+        a.target = '_blank';
+        a.rel = 'noopener';
+        htmlOutput = a;
+      } else if (html) {
+        const fragment = document.createDocumentFragment();
+        // Create temporary div to hold the nodes in.
+        const div = document.createElement('div');
+        div.innerHTML = html;
+        while (div.firstChild) {
+          fragment.appendChild(div.firstChild);
+        }
+        htmlOutput = fragment;
       }
-      fragment.insertBefore(item.html, fragment.firstChild);
-      cursor = item.position;
-    });
 
-    // Add the beginning portion at the end.
-    if (cursor !== 0) {
-      fragment.insertBefore(
-          document.createTextNode(text.slice(0, cursor)), fragment.firstChild);
-    }
-
-    this.callback(null, null, fragment);
-  };
-
-  /**
-   * Sort the given array of CommentLinkItems such that the positions are in
-   * reverse order.
-   *
-   * @param {!Array<Gerrit.CommentLinkItem>} outputArray
-   */
-  GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) {
-    outputArray.sort((a, b) => b.position - a.position);
-  };
-
-  /**
-   * Create a CommentLinkItem and append it to the given output array. This
-   * method can be called in either of two ways:
-   * - With `text` and `href` parameters provided, and the `html` parameter
-   *   passed as `null`. In this case, the new CommentLinkItem will be a link
-   *   element with the given text and href value.
-   * - With the `html` paremeter provided, and the `text` and `href` parameters
-   *   passed as `null`. In this case, the string of HTML will be parsed and the
-   *   first resulting node will be used as the resulting content.
-   *
-   * @param {string|null} text The text to use if creating a link.
-   * @param {string|null} href The href to use as the URL if creating a link.
-   * @param {string|null} html The html to parse and use as the result.
-   * @param {number} position The position inside the source text where the item
-   *     starts.
-   * @param {number} length The number of characters in the source text
-   *     represented by the item.
-   * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
-   *     new item is to be appended.
-   */
-  GrLinkTextParser.prototype.addItem =
-      function(text, href, html, position, length, outputArray) {
-        let htmlOutput = '';
-
-        if (href) {
-          const a = document.createElement('a');
-          a.href = href;
-          a.textContent = text;
-          a.target = '_blank';
-          a.rel = 'noopener';
-          htmlOutput = a;
-        } else if (html) {
-          const fragment = document.createDocumentFragment();
-          // Create temporary div to hold the nodes in.
-          const div = document.createElement('div');
-          div.innerHTML = html;
-          while (div.firstChild) {
-            fragment.appendChild(div.firstChild);
-          }
-          htmlOutput = fragment;
-        }
-
-        outputArray.push({
-          html: htmlOutput,
-          position,
-          length,
-        });
-      };
-
-  /**
-   * Create a CommentLinkItem for a link and append it to the given output
-   * array.
-   *
-   * @param {string|null} text The text for the link.
-   * @param {string|null} href The href to use as the URL of the link.
-   * @param {number} position The position inside the source text where the link
-   *     starts.
-   * @param {number} length The number of characters in the source text
-   *     represented by the link.
-   * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
-   *     new item is to be appended.
-   */
-  GrLinkTextParser.prototype.addLink =
-      function(text, href, position, length, outputArray) {
-        if (!text || this.hasOverlap(position, length, outputArray)) { return; }
-        if (!!this.baseUrl && href.startsWith('/') &&
-             !href.startsWith(this.baseUrl)) {
-          href = this.baseUrl + href;
-        }
-        this.addItem(text, href, null, position, length, outputArray);
-      };
-
-  /**
-   * Create a CommentLinkItem specified by an HTMl string and append it to the
-   * given output array.
-   *
-   * @param {string|null} html The html to parse and use as the result.
-   * @param {number} position The position inside the source text where the item
-   *     starts.
-   * @param {number} length The number of characters in the source text
-   *     represented by the item.
-   * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
-   *     new item is to be appended.
-   */
-  GrLinkTextParser.prototype.addHTML =
-      function(html, position, length, outputArray) {
-        if (this.hasOverlap(position, length, outputArray)) { return; }
-        if (!!this.baseUrl && html.match(/<a href=\"\//g) &&
-             !new RegExp(`<a href="${this.baseUrl}`, 'g').test(html)) {
-          html = html.replace(/<a href=\"\//g, `<a href=\"${this.baseUrl}\/`);
-        }
-        this.addItem(null, null, html, position, length, outputArray);
-      };
-
-  /**
-   * Does the given range overlap with anything already in the item list.
-   *
-   * @param {number} position
-   * @param {number} length
-   * @param {!Array<Gerrit.CommentLinkItem>} outputArray
-   */
-  GrLinkTextParser.prototype.hasOverlap =
-      function(position, length, outputArray) {
-        const endPosition = position + length;
-        for (let i = 0; i < outputArray.length; i++) {
-          const arrayItemStart = outputArray[i].position;
-          const arrayItemEnd = outputArray[i].position + outputArray[i].length;
-          if ((position >= arrayItemStart && position < arrayItemEnd) ||
-        (endPosition > arrayItemStart && endPosition <= arrayItemEnd) ||
-        (position === arrayItemStart && position === arrayItemEnd)) {
-            return true;
-          }
-        }
-        return false;
-      };
-
-  /**
-   * Parse the given source text and emit callbacks for the items that are
-   * parsed.
-   *
-   * @param {string} text
-   */
-  GrLinkTextParser.prototype.parse = function(text) {
-    if (text) {
-      linkify(text, {
-        callback: this.parseChunk.bind(this),
+      outputArray.push({
+        html: htmlOutput,
+        position,
+        length,
       });
-    }
-  };
+    };
 
-  /**
-   * Callback that is pased into the linkify function. ba-linkify will call this
-   * method in either of two ways:
-   * - With both a `text` and `href` parameter provided: this indicates that
-   *   ba-linkify has found a plain URL and wants it linkified.
-   * - With only a `text` parameter provided: this represents the non-link
-   *   content that lies between the links the library has found.
-   *
-   * @param {string} text
-   * @param {string|null|undefined} href
-   */
-  GrLinkTextParser.prototype.parseChunk = function(text, href) {
-    // TODO(wyatta) switch linkify sequence, see issue 5526.
-    if (this.removeZeroWidthSpace) {
-      // Remove the zero-width space added in gr-change-view.
-      text = text.replace(/^(CC|R)=\u200B/gm, '$1=');
-    }
+/**
+ * Create a CommentLinkItem for a link and append it to the given output
+ * array.
+ *
+ * @param {string|null} text The text for the link.
+ * @param {string|null} href The href to use as the URL of the link.
+ * @param {number} position The position inside the source text where the link
+ *     starts.
+ * @param {number} length The number of characters in the source text
+ *     represented by the link.
+ * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
+ *     new item is to be appended.
+ */
+GrLinkTextParser.prototype.addLink =
+    function(text, href, position, length, outputArray) {
+      if (!text || this.hasOverlap(position, length, outputArray)) { return; }
+      if (!!this.baseUrl && href.startsWith('/') &&
+           !href.startsWith(this.baseUrl)) {
+        href = this.baseUrl + href;
+      }
+      this.addItem(text, href, null, position, length, outputArray);
+    };
 
-    // If the href is provided then ba-linkify has recognized it as a URL. If
-    // the source text does not include a protocol, the protocol will be added
-    // by ba-linkify. Create the link if the href is provided and its protocol
-    // matches the expected pattern.
-    if (href) {
-      const result = URL_PROTOCOL_PATTERN.exec(href);
-      if (result) {
-        const prefixText = result[1];
-        if (prefixText.length > 0) {
-          // Fix for simple cases from
-          // https://bugs.chromium.org/p/gerrit/issues/detail?id=11697
-          // When leading whitespace is missed before link,
-          // linkify add this text before link as a schema name to href.
-          // We suppose, that prefixText just a single word
-          // before link and add this word as is, without processing
-          // any patterns in it.
-          this.parseLinks(prefixText, []);
-          text = text.substring(prefixText.length);
-          href = href.substring(prefixText.length);
+/**
+ * Create a CommentLinkItem specified by an HTMl string and append it to the
+ * given output array.
+ *
+ * @param {string|null} html The html to parse and use as the result.
+ * @param {number} position The position inside the source text where the item
+ *     starts.
+ * @param {number} length The number of characters in the source text
+ *     represented by the item.
+ * @param {!Array<Gerrit.CommentLinkItem>} outputArray The array to which the
+ *     new item is to be appended.
+ */
+GrLinkTextParser.prototype.addHTML =
+    function(html, position, length, outputArray) {
+      if (this.hasOverlap(position, length, outputArray)) { return; }
+      if (!!this.baseUrl && html.match(/<a href=\"\//g) &&
+           !new RegExp(`<a href="${this.baseUrl}`, 'g').test(html)) {
+        html = html.replace(/<a href=\"\//g, `<a href=\"${this.baseUrl}\/`);
+      }
+      this.addItem(null, null, html, position, length, outputArray);
+    };
+
+/**
+ * Does the given range overlap with anything already in the item list.
+ *
+ * @param {number} position
+ * @param {number} length
+ * @param {!Array<Gerrit.CommentLinkItem>} outputArray
+ */
+GrLinkTextParser.prototype.hasOverlap =
+    function(position, length, outputArray) {
+      const endPosition = position + length;
+      for (let i = 0; i < outputArray.length; i++) {
+        const arrayItemStart = outputArray[i].position;
+        const arrayItemEnd = outputArray[i].position + outputArray[i].length;
+        if ((position >= arrayItemStart && position < arrayItemEnd) ||
+      (endPosition > arrayItemStart && endPosition <= arrayItemEnd) ||
+      (position === arrayItemStart && position === arrayItemEnd)) {
+          return true;
         }
-        this.addText(text, href);
-        return;
+      }
+      return false;
+    };
+
+/**
+ * Parse the given source text and emit callbacks for the items that are
+ * parsed.
+ *
+ * @param {string} text
+ */
+GrLinkTextParser.prototype.parse = function(text) {
+  if (text) {
+    linkify(text, {
+      callback: this.parseChunk.bind(this),
+    });
+  }
+};
+
+/**
+ * Callback that is pased into the linkify function. ba-linkify will call this
+ * method in either of two ways:
+ * - With both a `text` and `href` parameter provided: this indicates that
+ *   ba-linkify has found a plain URL and wants it linkified.
+ * - With only a `text` parameter provided: this represents the non-link
+ *   content that lies between the links the library has found.
+ *
+ * @param {string} text
+ * @param {string|null|undefined} href
+ */
+GrLinkTextParser.prototype.parseChunk = function(text, href) {
+  // TODO(wyatta) switch linkify sequence, see issue 5526.
+  if (this.removeZeroWidthSpace) {
+    // Remove the zero-width space added in gr-change-view.
+    text = text.replace(/^(CC|R)=\u200B/gm, '$1=');
+  }
+
+  // If the href is provided then ba-linkify has recognized it as a URL. If
+  // the source text does not include a protocol, the protocol will be added
+  // by ba-linkify. Create the link if the href is provided and its protocol
+  // matches the expected pattern.
+  if (href) {
+    const result = URL_PROTOCOL_PATTERN.exec(href);
+    if (result) {
+      const prefixText = result[1];
+      if (prefixText.length > 0) {
+        // Fix for simple cases from
+        // https://bugs.chromium.org/p/gerrit/issues/detail?id=11697
+        // When leading whitespace is missed before link,
+        // linkify add this text before link as a schema name to href.
+        // We suppose, that prefixText just a single word
+        // before link and add this word as is, without processing
+        // any patterns in it.
+        this.parseLinks(prefixText, []);
+        text = text.substring(prefixText.length);
+        href = href.substring(prefixText.length);
+      }
+      this.addText(text, href);
+      return;
+    }
+  }
+  // For the sections of text that lie between the links found by
+  // ba-linkify, we search for the project-config-specified link patterns.
+  this.parseLinks(text, this.linkConfig);
+};
+
+/**
+ * Walk over the given source text to find matches for comemntlink patterns
+ * and emit parse result callbacks.
+ *
+ * @param {string} text The raw source text.
+ * @param {Object|null|undefined} patterns A comment links specification
+ *   object.
+ */
+GrLinkTextParser.prototype.parseLinks = function(text, patterns) {
+  // The outputArray is used to store all of the matches found for all
+  // patterns.
+  const outputArray = [];
+  for (const p in patterns) {
+    if (patterns[p].enabled != null && patterns[p].enabled == false) {
+      continue;
+    }
+    // PolyGerrit doesn't use hash-based navigation like the GWT UI.
+    // Account for this.
+    if (patterns[p].html) {
+      patterns[p].html =
+          patterns[p].html.replace(/<a href=\"#\//g, '<a href="/');
+    } else if (patterns[p].link) {
+      if (patterns[p].link[0] == '#') {
+        patterns[p].link = patterns[p].link.substr(1);
       }
     }
-    // For the sections of text that lie between the links found by
-    // ba-linkify, we search for the project-config-specified link patterns.
-    this.parseLinks(text, this.linkConfig);
-  };
 
-  /**
-   * Walk over the given source text to find matches for comemntlink patterns
-   * and emit parse result callbacks.
-   *
-   * @param {string} text The raw source text.
-   * @param {Object|null|undefined} patterns A comment links specification
-   *   object.
-   */
-  GrLinkTextParser.prototype.parseLinks = function(text, patterns) {
-    // The outputArray is used to store all of the matches found for all
-    // patterns.
-    const outputArray = [];
-    for (const p in patterns) {
-      if (patterns[p].enabled != null && patterns[p].enabled == false) {
-        continue;
-      }
-      // PolyGerrit doesn't use hash-based navigation like the GWT UI.
-      // Account for this.
+    const pattern = new RegExp(patterns[p].match, 'g');
+
+    let match;
+    let textToCheck = text;
+    let susbtrIndex = 0;
+
+    while ((match = pattern.exec(textToCheck)) != null) {
+      textToCheck = textToCheck.substr(match.index + match[0].length);
+      let result = match[0].replace(pattern,
+          patterns[p].html || patterns[p].link);
+
       if (patterns[p].html) {
-        patterns[p].html =
-            patterns[p].html.replace(/<a href=\"#\//g, '<a href="/');
+        let i;
+        // Skip portion of replacement string that is equal to original to
+        // allow overlapping patterns.
+        for (i = 0; i < result.length; i++) {
+          if (result[i] !== match[0][i]) { break; }
+        }
+        result = result.slice(i);
+
+        this.addHTML(
+            result,
+            susbtrIndex + match.index + i,
+            match[0].length - i,
+            outputArray);
       } else if (patterns[p].link) {
-        if (patterns[p].link[0] == '#') {
-          patterns[p].link = patterns[p].link.substr(1);
-        }
+        this.addLink(
+            match[0],
+            result,
+            susbtrIndex + match.index,
+            match[0].length,
+            outputArray);
+      } else {
+        throw Error('linkconfig entry ' + p +
+            ' doesn’t contain a link or html attribute.');
       }
 
-      const pattern = new RegExp(patterns[p].match, 'g');
-
-      let match;
-      let textToCheck = text;
-      let susbtrIndex = 0;
-
-      while ((match = pattern.exec(textToCheck)) != null) {
-        textToCheck = textToCheck.substr(match.index + match[0].length);
-        let result = match[0].replace(pattern,
-            patterns[p].html || patterns[p].link);
-
-        if (patterns[p].html) {
-          let i;
-          // Skip portion of replacement string that is equal to original to
-          // allow overlapping patterns.
-          for (i = 0; i < result.length; i++) {
-            if (result[i] !== match[0][i]) { break; }
-          }
-          result = result.slice(i);
-
-          this.addHTML(
-              result,
-              susbtrIndex + match.index + i,
-              match[0].length - i,
-              outputArray);
-        } else if (patterns[p].link) {
-          this.addLink(
-              match[0],
-              result,
-              susbtrIndex + match.index,
-              match[0].length,
-              outputArray);
-        } else {
-          throw Error('linkconfig entry ' + p +
-              ' doesn’t contain a link or html attribute.');
-        }
-
-        // Update the substring location so we know where we are in relation to
-        // the initial full text string.
-        susbtrIndex = susbtrIndex + match.index + match[0].length;
-      }
+      // Update the substring location so we know where we are in relation to
+      // the initial full text string.
+      susbtrIndex = susbtrIndex + match.index + match[0].length;
     }
-    this.processLinks(text, outputArray);
-  };
-
-  window.GrLinkTextParser = GrLinkTextParser;
-})();
+  }
+  this.processLinks(text, outputArray);
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 33c8d8e..23b8de7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -24,92 +24,83 @@
 
 document.head.appendChild($_documentContainer.content);
 
-(function(window) {
-  'use strict';
+// Limit cache size because /change/detail responses may be large.
+const MAX_CACHE_SIZE = 30;
 
-  // Prevent redefinition.
-  if (window.GrEtagDecorator) { return; }
+/** @constructor */
+export function GrEtagDecorator() {
+  this._etags = new Map();
+  this._payloadCache = new Map();
+}
 
-  // Limit cache size because /change/detail responses may be large.
-  const MAX_CACHE_SIZE = 30;
-
-  /** @constructor */
-  function GrEtagDecorator() {
-    this._etags = new Map();
-    this._payloadCache = new Map();
+/**
+ * Get or upgrade fetch options to include an ETag in a request.
+ *
+ * @param {string} url The URL being fetched.
+ * @param {!Object=} opt_options Optional options object in which to include
+ *     the ETag request header. If omitted, the result will be a fresh option
+ *     set.
+ * @return {!Object}
+ */
+GrEtagDecorator.prototype.getOptions = function(url, opt_options) {
+  const etag = this._etags.get(url);
+  if (!etag) {
+    return opt_options;
   }
+  const options = Object.assign({}, opt_options);
+  options.headers = options.headers || new Headers();
+  options.headers.set('If-None-Match', this._etags.get(url));
+  return options;
+};
 
-  /**
-   * Get or upgrade fetch options to include an ETag in a request.
-   *
-   * @param {string} url The URL being fetched.
-   * @param {!Object=} opt_options Optional options object in which to include
-   *     the ETag request header. If omitted, the result will be a fresh option
-   *     set.
-   * @return {!Object}
-   */
-  GrEtagDecorator.prototype.getOptions = function(url, opt_options) {
-    const etag = this._etags.get(url);
-    if (!etag) {
-      return opt_options;
+/**
+ * Handle a response to a request with ETag headers, potentially incorporating
+ * its result in the payload cache.
+ *
+ * @param {string} url The URL of the request.
+ * @param {!Response} response The response object.
+ * @param {string} payload The raw, unparsed JSON contained in the response
+ *     body. Note: because response.text() cannot be read twice, this must be
+ *     provided separately.
+ */
+GrEtagDecorator.prototype.collect = function(url, response, payload) {
+  if (!response ||
+      !response.ok ||
+      response.status !== 200 ||
+      response.status === 304) {
+    // 304 Not Modified means etag is still valid.
+    return;
+  }
+  this._payloadCache.set(url, payload);
+  const etag = response.headers && response.headers.get('etag');
+  if (!etag) {
+    this._etags.delete(url);
+  } else {
+    this._etags.set(url, etag);
+    this._truncateCache();
+  }
+};
+
+/**
+ * Get the cached payload for a given URL.
+ *
+ * @param {string} url
+ * @return {string|undefined} Returns the unparsed JSON payload from the
+ *     cache.
+ */
+GrEtagDecorator.prototype.getCachedPayload = function(url) {
+  return this._payloadCache.get(url);
+};
+
+/**
+ * Limit the cache size to MAX_CACHE_SIZE.
+ */
+GrEtagDecorator.prototype._truncateCache = function() {
+  for (const url of this._etags.keys()) {
+    if (this._etags.size <= MAX_CACHE_SIZE) {
+      break;
     }
-    const options = Object.assign({}, opt_options);
-    options.headers = options.headers || new Headers();
-    options.headers.set('If-None-Match', this._etags.get(url));
-    return options;
-  };
-
-  /**
-   * Handle a response to a request with ETag headers, potentially incorporating
-   * its result in the payload cache.
-   *
-   * @param {string} url The URL of the request.
-   * @param {!Response} response The response object.
-   * @param {string} payload The raw, unparsed JSON contained in the response
-   *     body. Note: because response.text() cannot be read twice, this must be
-   *     provided separately.
-   */
-  GrEtagDecorator.prototype.collect = function(url, response, payload) {
-    if (!response ||
-        !response.ok ||
-        response.status !== 200 ||
-        response.status === 304) {
-      // 304 Not Modified means etag is still valid.
-      return;
-    }
-    this._payloadCache.set(url, payload);
-    const etag = response.headers && response.headers.get('etag');
-    if (!etag) {
-      this._etags.delete(url);
-    } else {
-      this._etags.set(url, etag);
-      this._truncateCache();
-    }
-  };
-
-  /**
-   * Get the cached payload for a given URL.
-   *
-   * @param {string} url
-   * @return {string|undefined} Returns the unparsed JSON payload from the
-   *     cache.
-   */
-  GrEtagDecorator.prototype.getCachedPayload = function(url) {
-    return this._payloadCache.get(url);
-  };
-
-  /**
-   * Limit the cache size to MAX_CACHE_SIZE.
-   */
-  GrEtagDecorator.prototype._truncateCache = function() {
-    for (const url of this._etags.keys()) {
-      if (this._etags.size <= MAX_CACHE_SIZE) {
-        break;
-      }
-      this._etags.delete(url);
-      this._payloadCache.delete(url);
-    }
-  };
-
-  window.GrEtagDecorator = GrEtagDecorator;
-})(window);
+    this._etags.delete(url);
+    this._payloadCache.delete(url);
+  }
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
index 482dc6a..f82779a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -25,7 +25,8 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script type="module">
 import '../../../test/common-test-setup.js';
-import './gr-etag-decorator.js';
+import {GrEtagDecorator} from './gr-etag-decorator.js';
+
 suite('gr-etag-decorator', () => {
   let etag;
   let sandbox;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 452d4a1..be39828 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -23,10 +23,7 @@
 */
 import '../../../scripts/bundled-polymer.js';
 
-import './gr-etag-decorator.js';
-import './gr-rest-apis/gr-rest-api-helper.js';
 import './gr-auth.js';
-import './gr-reviewer-updates-parser.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -36,6 +33,9 @@
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {GrEtagDecorator} from './gr-etag-decorator.js';
+import {SiteBasedCache, FetchPromisesCache, GrRestApiHelper} from './gr-rest-apis/gr-rest-api-helper.js';
+import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
 
 const DiffViewMode = {
   SIDE_BY_SIDE: 'SIDE_BY_SIDE',
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 77a8fe6..d7a128a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -35,6 +35,8 @@
 import '../../../scripts/util.js';
 import './gr-rest-api-interface.js';
 import {mockPromise} from '../../../test/test-utils.js';
+import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
+
 suite('gr-rest-api-interface tests', () => {
   let element;
   let sandbox;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
index 7368f8f..bc70791 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
@@ -14,401 +14,393 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
+const JSON_PREFIX = ')]}\'';
 
-  const JSON_PREFIX = ')]}\'';
+/**
+ * Wrapper around Map for caching server responses. Site-based so that
+ * changes to CANONICAL_PATH will result in a different cache going into
+ * effect.
+ */
+export class SiteBasedCache {
+  constructor() {
+    // Container of per-canonical-path caches.
+    this._data = new Map();
+    if (window.INITIAL_DATA != undefined) {
+      // Put all data shipped with index.html into the cache. This makes it
+      // so that we spare more round trips to the server when the app loads
+      // initially.
+      Object
+          .entries(window.INITIAL_DATA)
+          .forEach(e => this._cache().set(e[0], e[1]));
+    }
+  }
+
+  // Returns the cache for the current canonical path.
+  _cache() {
+    if (!this._data.has(window.CANONICAL_PATH)) {
+      this._data.set(window.CANONICAL_PATH, new Map());
+    }
+    return this._data.get(window.CANONICAL_PATH);
+  }
+
+  has(key) {
+    return this._cache().has(key);
+  }
+
+  get(key) {
+    return this._cache().get(key);
+  }
+
+  set(key, value) {
+    this._cache().set(key, value);
+  }
+
+  delete(key) {
+    this._cache().delete(key);
+  }
+
+  invalidatePrefix(prefix) {
+    const newMap = new Map();
+    for (const [key, value] of this._cache().entries()) {
+      if (!key.startsWith(prefix)) {
+        newMap.set(key, value);
+      }
+    }
+    this._data.set(window.CANONICAL_PATH, newMap);
+  }
+}
+
+export class FetchPromisesCache {
+  constructor() {
+    this._data = {};
+  }
+
+  has(key) {
+    return !!this._data[key];
+  }
+
+  get(key) {
+    return this._data[key];
+  }
+
+  set(key, value) {
+    this._data[key] = value;
+  }
+
+  invalidatePrefix(prefix) {
+    const newData = {};
+    Object.entries(this._data).forEach(([key, value]) => {
+      if (!key.startsWith(prefix)) {
+        newData[key] = value;
+      }
+    });
+    this._data = newData;
+  }
+}
+
+export class GrRestApiHelper {
+  /**
+   * @param {SiteBasedCache} cache
+   * @param {object} auth
+   * @param {FetchPromisesCache} fetchPromisesCache
+   * @param {object} restApiInterface
+   */
+  constructor(cache, auth, fetchPromisesCache,
+      restApiInterface) {
+    this._cache = cache;// TODO: make it public
+    this._auth = auth;
+    this._fetchPromisesCache = fetchPromisesCache;
+    this._restApiInterface = restApiInterface;
+  }
 
   /**
-   * Wrapper around Map for caching server responses. Site-based so that
-   * changes to CANONICAL_PATH will result in a different cache going into
-   * effect.
+   * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
+   * with timing and logging.
+   *
+   * @param {Gerrit.FetchRequest} req
    */
-  class SiteBasedCache {
-    constructor() {
-      // Container of per-canonical-path caches.
-      this._data = new Map();
-      if (window.INITIAL_DATA != undefined) {
-        // Put all data shipped with index.html into the cache. This makes it
-        // so that we spare more round trips to the server when the app loads
-        // initially.
-        Object
-            .entries(window.INITIAL_DATA)
-            .forEach(e => this._cache().set(e[0], e[1]));
-      }
-    }
+  fetch(req) {
+    const start = Date.now();
+    const xhr = this._auth.fetch(req.url, req.fetchOptions);
 
-    // Returns the cache for the current canonical path.
-    _cache() {
-      if (!this._data.has(window.CANONICAL_PATH)) {
-        this._data.set(window.CANONICAL_PATH, new Map());
-      }
-      return this._data.get(window.CANONICAL_PATH);
-    }
+    // Log the call after it completes.
+    xhr.then(res => this._logCall(req, start, res ? res.status : null));
 
-    has(key) {
-      return this._cache().has(key);
-    }
+    // Return the XHR directly (without the log).
+    return xhr;
+  }
 
-    get(key) {
-      return this._cache().get(key);
-    }
-
-    set(key, value) {
-      this._cache().set(key, value);
-    }
-
-    delete(key) {
-      this._cache().delete(key);
-    }
-
-    invalidatePrefix(prefix) {
-      const newMap = new Map();
-      for (const [key, value] of this._cache().entries()) {
-        if (!key.startsWith(prefix)) {
-          newMap.set(key, value);
-        }
-      }
-      this._data.set(window.CANONICAL_PATH, newMap);
+  /**
+   * Log information about a REST call. Because the elapsed time is determined
+   * by this method, it should be called immediately after the request
+   * finishes.
+   *
+   * @param {Gerrit.FetchRequest} req
+   * @param {number} startTime the time that the request was started.
+   * @param {number} status the HTTP status of the response. The status value
+   *     is used here rather than the response object so there is no way this
+   *     method can read the body stream.
+   */
+  _logCall(req, startTime, status) {
+    const method = (req.fetchOptions && req.fetchOptions.method) ?
+      req.fetchOptions.method : 'GET';
+    const endTime = Date.now();
+    const elapsed = (endTime - startTime);
+    const startAt = new Date(startTime);
+    const endAt = new Date(endTime);
+    console.log([
+      'HTTP',
+      status,
+      method,
+      elapsed + 'ms',
+      req.anonymizedUrl || req.url,
+      `(${startAt.toISOString()}, ${endAt.toISOString()})`,
+    ].join(' '));
+    if (req.anonymizedUrl) {
+      this.dispatchEvent(new CustomEvent('rpc-log', {
+        detail: {status, method, elapsed, anonymizedUrl: req.anonymizedUrl},
+        composed: true, bubbles: true,
+      }));
     }
   }
 
-  class FetchPromisesCache {
-    constructor() {
-      this._data = {};
-    }
-
-    has(key) {
-      return !!this._data[key];
-    }
-
-    get(key) {
-      return this._data[key];
-    }
-
-    set(key, value) {
-      this._data[key] = value;
-    }
-
-    invalidatePrefix(prefix) {
-      const newData = {};
-      Object.entries(this._data).forEach(([key, value]) => {
-        if (!key.startsWith(prefix)) {
-          newData[key] = value;
-        }
-      });
-      this._data = newData;
-    }
-  }
-
-  class GrRestApiHelper {
-    /**
-     * @param {SiteBasedCache} cache
-     * @param {object} auth
-     * @param {FetchPromisesCache} fetchPromisesCache
-     * @param {object} restApiInterface
-     */
-    constructor(cache, auth, fetchPromisesCache,
-        restApiInterface) {
-      this._cache = cache;// TODO: make it public
-      this._auth = auth;
-      this._fetchPromisesCache = fetchPromisesCache;
-      this._restApiInterface = restApiInterface;
-    }
-
-    /**
-     * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
-     * with timing and logging.
-     *
-     * @param {Gerrit.FetchRequest} req
-     */
-    fetch(req) {
-      const start = Date.now();
-      const xhr = this._auth.fetch(req.url, req.fetchOptions);
-
-      // Log the call after it completes.
-      xhr.then(res => this._logCall(req, start, res ? res.status : null));
-
-      // Return the XHR directly (without the log).
-      return xhr;
-    }
-
-    /**
-     * Log information about a REST call. Because the elapsed time is determined
-     * by this method, it should be called immediately after the request
-     * finishes.
-     *
-     * @param {Gerrit.FetchRequest} req
-     * @param {number} startTime the time that the request was started.
-     * @param {number} status the HTTP status of the response. The status value
-     *     is used here rather than the response object so there is no way this
-     *     method can read the body stream.
-     */
-    _logCall(req, startTime, status) {
-      const method = (req.fetchOptions && req.fetchOptions.method) ?
-        req.fetchOptions.method : 'GET';
-      const endTime = Date.now();
-      const elapsed = (endTime - startTime);
-      const startAt = new Date(startTime);
-      const endAt = new Date(endTime);
-      console.log([
-        'HTTP',
-        status,
-        method,
-        elapsed + 'ms',
-        req.anonymizedUrl || req.url,
-        `(${startAt.toISOString()}, ${endAt.toISOString()})`,
-      ].join(' '));
-      if (req.anonymizedUrl) {
-        this.dispatchEvent(new CustomEvent('rpc-log', {
-          detail: {status, method, elapsed, anonymizedUrl: req.anonymizedUrl},
-          composed: true, bubbles: true,
-        }));
-      }
-    }
-
-    /**
-     * Fetch JSON from url provided.
-     * Returns a Promise that resolves to a native Response.
-     * Doesn't do error checking. Supports cancel condition. Performs auth.
-     * Validates auth expiry errors.
-     *
-     * @param {Gerrit.FetchJSONRequest} req
-     */
-    fetchRawJSON(req) {
-      const urlWithParams = this.urlWithParams(req.url, req.params);
-      const fetchReq = {
-        url: urlWithParams,
-        fetchOptions: req.fetchOptions,
-        anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
-      };
-      return this.fetch(fetchReq)
-          .then(res => {
-            if (req.cancelCondition && req.cancelCondition()) {
-              res.body.cancel();
-              return;
-            }
-            return res;
-          })
-          .catch(err => {
-            if (req.errFn) {
-              req.errFn.call(undefined, null, err);
-            } else {
-              this.dispatchEvent(new CustomEvent('network-error', {
-                detail: {error: err},
-                composed: true, bubbles: true,
-              }));
-            }
-            throw err;
-          });
-    }
-
-    /**
-     * Fetch JSON from url provided.
-     * Returns a Promise that resolves to a parsed response.
-     * Same as {@link fetchRawJSON}, plus error handling.
-     *
-     * @param {Gerrit.FetchJSONRequest} req
-     * @param {boolean} noAcceptHeader - don't add default accept json header
-     */
-    fetchJSON(req, noAcceptHeader) {
-      if (!noAcceptHeader) {
-        req = this.addAcceptJsonHeader(req);
-      }
-      return this.fetchRawJSON(req).then(response => {
-        if (!response) {
-          return;
-        }
-        if (!response.ok) {
-          if (req.errFn) {
-            req.errFn.call(null, response);
+  /**
+   * Fetch JSON from url provided.
+   * Returns a Promise that resolves to a native Response.
+   * Doesn't do error checking. Supports cancel condition. Performs auth.
+   * Validates auth expiry errors.
+   *
+   * @param {Gerrit.FetchJSONRequest} req
+   */
+  fetchRawJSON(req) {
+    const urlWithParams = this.urlWithParams(req.url, req.params);
+    const fetchReq = {
+      url: urlWithParams,
+      fetchOptions: req.fetchOptions,
+      anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
+    };
+    return this.fetch(fetchReq)
+        .then(res => {
+          if (req.cancelCondition && req.cancelCondition()) {
+            res.body.cancel();
             return;
           }
-          this.dispatchEvent(new CustomEvent('server-error', {
-            detail: {request: req, response},
-            composed: true, bubbles: true,
-          }));
-          return;
-        }
-        return response && this.getResponseObject(response);
-      });
-    }
-
-    /**
-     * @param {string} url
-     * @param {?Object|string=} opt_params URL params, key-value hash.
-     * @return {string}
-     */
-    urlWithParams(url, opt_params) {
-      if (!opt_params) { return this.getBaseUrl() + url; }
-
-      const params = [];
-      for (const p in opt_params) {
-        if (!opt_params.hasOwnProperty(p)) { continue; }
-        if (opt_params[p] == null) {
-          params.push(encodeURIComponent(p));
-          continue;
-        }
-        for (const value of [].concat(opt_params[p])) {
-          params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
-        }
-      }
-      return this.getBaseUrl() + url + '?' + params.join('&');
-    }
-
-    /**
-     * @param {!Object} response
-     * @return {?}
-     */
-    getResponseObject(response) {
-      return this.readResponsePayload(response)
-          .then(payload => payload.parsed);
-    }
-
-    /**
-     * @param {!Object} response
-     * @return {!Object}
-     */
-    readResponsePayload(response) {
-      return response.text().then(text => {
-        let result;
-        try {
-          result = this.parsePrefixedJSON(text);
-        } catch (_) {
-          result = null;
-        }
-        return {parsed: result, raw: text};
-      });
-    }
-
-    /**
-     * @param {string} source
-     * @return {?}
-     */
-    parsePrefixedJSON(source) {
-      return JSON.parse(source.substring(JSON_PREFIX.length));
-    }
-
-    /**
-     * @param {Gerrit.FetchJSONRequest} req
-     * @return {Gerrit.FetchJSONRequest}
-     */
-    addAcceptJsonHeader(req) {
-      if (!req.fetchOptions) req.fetchOptions = {};
-      if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
-      if (!req.fetchOptions.headers.has('Accept')) {
-        req.fetchOptions.headers.append('Accept', 'application/json');
-      }
-      return req;
-    }
-
-    getBaseUrl() {
-      return this._restApiInterface.getBaseUrl();
-    }
-
-    dispatchEvent(type, detail) {
-      return this._restApiInterface.dispatchEvent(type, detail);
-    }
-
-    /**
-     * @param {Gerrit.FetchJSONRequest} req
-     */
-    fetchCacheURL(req) {
-      if (this._fetchPromisesCache.has(req.url)) {
-        return this._fetchPromisesCache.get(req.url);
-      }
-      // TODO(andybons): Periodic cache invalidation.
-      if (this._cache.has(req.url)) {
-        return Promise.resolve(this._cache.get(req.url));
-      }
-      this._fetchPromisesCache.set(req.url,
-          this.fetchJSON(req)
-              .then(response => {
-                if (response !== undefined) {
-                  this._cache.set(req.url, response);
-                }
-                this._fetchPromisesCache.set(req.url, undefined);
-                return response;
-              })
-              .catch(err => {
-                this._fetchPromisesCache.set(req.url, undefined);
-                throw err;
-              })
-      );
-      return this._fetchPromisesCache.get(req.url);
-    }
-
-    /**
-     * Send an XHR.
-     *
-     * @param {Gerrit.SendRequest} req
-     * @return {Promise}
-     */
-    send(req) {
-      const options = {method: req.method};
-      if (req.body) {
-        options.headers = new Headers();
-        options.headers.set(
-            'Content-Type', req.contentType || 'application/json');
-        options.body = typeof req.body === 'string' ?
-          req.body : JSON.stringify(req.body);
-      }
-      if (req.headers) {
-        if (!options.headers) { options.headers = new Headers(); }
-        for (const header in req.headers) {
-          if (!req.headers.hasOwnProperty(header)) { continue; }
-          options.headers.set(header, req.headers[header]);
-        }
-      }
-      const url = req.url.startsWith('http') ?
-        req.url : this.getBaseUrl() + req.url;
-      const fetchReq = {
-        url,
-        fetchOptions: options,
-        anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
-      };
-      const xhr = this.fetch(fetchReq)
-          .then(response => {
-            if (!response.ok) {
-              if (req.errFn) {
-                return req.errFn.call(undefined, response);
-              }
-              this.dispatchEvent(new CustomEvent('server-error', {
-                detail: {request: fetchReq, response},
-                composed: true, bubbles: true,
-              }));
-            }
-            return response;
-          })
-          .catch(err => {
+          return res;
+        })
+        .catch(err => {
+          if (req.errFn) {
+            req.errFn.call(undefined, null, err);
+          } else {
             this.dispatchEvent(new CustomEvent('network-error', {
               detail: {error: err},
               composed: true, bubbles: true,
             }));
-            if (req.errFn) {
-              return req.errFn.call(undefined, null, err);
-            } else {
-              throw err;
-            }
-          });
-
-      if (req.parseResponse) {
-        return xhr.then(res => this.getResponseObject(res));
-      }
-
-      return xhr;
-    }
-
-    /**
-     * @param {string} prefix
-     */
-    invalidateFetchPromisesPrefix(prefix) {
-      this._fetchPromisesCache.invalidatePrefix(prefix);
-      this._cache.invalidatePrefix(prefix);
-    }
+          }
+          throw err;
+        });
   }
 
-  window.SiteBasedCache = SiteBasedCache;
-  window.FetchPromisesCache = FetchPromisesCache;
-  window.GrRestApiHelper = GrRestApiHelper;
-})(window);
+  /**
+   * Fetch JSON from url provided.
+   * Returns a Promise that resolves to a parsed response.
+   * Same as {@link fetchRawJSON}, plus error handling.
+   *
+   * @param {Gerrit.FetchJSONRequest} req
+   * @param {boolean} noAcceptHeader - don't add default accept json header
+   */
+  fetchJSON(req, noAcceptHeader) {
+    if (!noAcceptHeader) {
+      req = this.addAcceptJsonHeader(req);
+    }
+    return this.fetchRawJSON(req).then(response => {
+      if (!response) {
+        return;
+      }
+      if (!response.ok) {
+        if (req.errFn) {
+          req.errFn.call(null, response);
+          return;
+        }
+        this.dispatchEvent(new CustomEvent('server-error', {
+          detail: {request: req, response},
+          composed: true, bubbles: true,
+        }));
+        return;
+      }
+      return response && this.getResponseObject(response);
+    });
+  }
+
+  /**
+   * @param {string} url
+   * @param {?Object|string=} opt_params URL params, key-value hash.
+   * @return {string}
+   */
+  urlWithParams(url, opt_params) {
+    if (!opt_params) { return this.getBaseUrl() + url; }
+
+    const params = [];
+    for (const p in opt_params) {
+      if (!opt_params.hasOwnProperty(p)) { continue; }
+      if (opt_params[p] == null) {
+        params.push(encodeURIComponent(p));
+        continue;
+      }
+      for (const value of [].concat(opt_params[p])) {
+        params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
+      }
+    }
+    return this.getBaseUrl() + url + '?' + params.join('&');
+  }
+
+  /**
+   * @param {!Object} response
+   * @return {?}
+   */
+  getResponseObject(response) {
+    return this.readResponsePayload(response)
+        .then(payload => payload.parsed);
+  }
+
+  /**
+   * @param {!Object} response
+   * @return {!Object}
+   */
+  readResponsePayload(response) {
+    return response.text().then(text => {
+      let result;
+      try {
+        result = this.parsePrefixedJSON(text);
+      } catch (_) {
+        result = null;
+      }
+      return {parsed: result, raw: text};
+    });
+  }
+
+  /**
+   * @param {string} source
+   * @return {?}
+   */
+  parsePrefixedJSON(source) {
+    return JSON.parse(source.substring(JSON_PREFIX.length));
+  }
+
+  /**
+   * @param {Gerrit.FetchJSONRequest} req
+   * @return {Gerrit.FetchJSONRequest}
+   */
+  addAcceptJsonHeader(req) {
+    if (!req.fetchOptions) req.fetchOptions = {};
+    if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
+    if (!req.fetchOptions.headers.has('Accept')) {
+      req.fetchOptions.headers.append('Accept', 'application/json');
+    }
+    return req;
+  }
+
+  getBaseUrl() {
+    return this._restApiInterface.getBaseUrl();
+  }
+
+  dispatchEvent(type, detail) {
+    return this._restApiInterface.dispatchEvent(type, detail);
+  }
+
+  /**
+   * @param {Gerrit.FetchJSONRequest} req
+   */
+  fetchCacheURL(req) {
+    if (this._fetchPromisesCache.has(req.url)) {
+      return this._fetchPromisesCache.get(req.url);
+    }
+    // TODO(andybons): Periodic cache invalidation.
+    if (this._cache.has(req.url)) {
+      return Promise.resolve(this._cache.get(req.url));
+    }
+    this._fetchPromisesCache.set(req.url,
+        this.fetchJSON(req)
+            .then(response => {
+              if (response !== undefined) {
+                this._cache.set(req.url, response);
+              }
+              this._fetchPromisesCache.set(req.url, undefined);
+              return response;
+            })
+            .catch(err => {
+              this._fetchPromisesCache.set(req.url, undefined);
+              throw err;
+            })
+    );
+    return this._fetchPromisesCache.get(req.url);
+  }
+
+  /**
+   * Send an XHR.
+   *
+   * @param {Gerrit.SendRequest} req
+   * @return {Promise}
+   */
+  send(req) {
+    const options = {method: req.method};
+    if (req.body) {
+      options.headers = new Headers();
+      options.headers.set(
+          'Content-Type', req.contentType || 'application/json');
+      options.body = typeof req.body === 'string' ?
+        req.body : JSON.stringify(req.body);
+    }
+    if (req.headers) {
+      if (!options.headers) { options.headers = new Headers(); }
+      for (const header in req.headers) {
+        if (!req.headers.hasOwnProperty(header)) { continue; }
+        options.headers.set(header, req.headers[header]);
+      }
+    }
+    const url = req.url.startsWith('http') ?
+      req.url : this.getBaseUrl() + req.url;
+    const fetchReq = {
+      url,
+      fetchOptions: options,
+      anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
+    };
+    const xhr = this.fetch(fetchReq)
+        .then(response => {
+          if (!response.ok) {
+            if (req.errFn) {
+              return req.errFn.call(undefined, response);
+            }
+            this.dispatchEvent(new CustomEvent('server-error', {
+              detail: {request: fetchReq, response},
+              composed: true, bubbles: true,
+            }));
+          }
+          return response;
+        })
+        .catch(err => {
+          this.dispatchEvent(new CustomEvent('network-error', {
+            detail: {error: err},
+            composed: true, bubbles: true,
+          }));
+          if (req.errFn) {
+            return req.errFn.call(undefined, null, err);
+          } else {
+            throw err;
+          }
+        });
+
+    if (req.parseResponse) {
+      return xhr.then(res => this.getResponseObject(res));
+    }
+
+    return xhr;
+  }
+
+  /**
+   * @param {string} prefix
+   */
+  invalidateFetchPromisesPrefix(prefix) {
+    this._fetchPromisesCache.invalidatePrefix(prefix);
+    this._cache.invalidatePrefix(prefix);
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
index 5ca70a3..fa0c849 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
@@ -28,7 +28,9 @@
 import '../../../../test/common-test-setup.js';
 import '../../../../scripts/util.js';
 import '../gr-auth.js';
-import './gr-rest-api-helper.js';
+import {SiteBasedCache} from './gr-rest-api-helper.js';
+import {FetchPromisesCache, GrRestApiHelper} from './gr-rest-api-helper.js';
+
 suite('gr-rest-api-helper tests', () => {
   let helper;
   let sandbox;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
index d599563..acaab73 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
@@ -14,221 +14,214 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  // Prevent redefinition.
-  if (window.GrReviewerUpdatesParser) { return; }
+/** @constructor */
+export function GrReviewerUpdatesParser(change) {
+  this.result = Object.assign({}, change);
+  this._lastState = {};
+}
 
-  /** @constructor */
-  function GrReviewerUpdatesParser(change) {
-    this.result = Object.assign({}, change);
-    this._lastState = {};
+GrReviewerUpdatesParser.parse = function(change) {
+  if (!change ||
+      !change.messages ||
+      !change.reviewer_updates ||
+      !change.reviewer_updates.length) {
+    return change;
   }
+  const parser = new GrReviewerUpdatesParser(change);
+  parser._filterRemovedMessages();
+  parser._groupUpdates();
+  parser._formatUpdates();
+  parser._advanceUpdates();
+  return parser.result;
+};
 
-  GrReviewerUpdatesParser.parse = function(change) {
-    if (!change ||
-        !change.messages ||
-        !change.reviewer_updates ||
-        !change.reviewer_updates.length) {
-      return change;
+GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS = 500;
+GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS = 6000;
+
+GrReviewerUpdatesParser.prototype.result = null;
+GrReviewerUpdatesParser.prototype._batch = null;
+GrReviewerUpdatesParser.prototype._updateItems = null;
+GrReviewerUpdatesParser.prototype._lastState = null;
+
+/**
+ * Removes messages that describe removed reviewers, since reviewer_updates
+ * are used.
+ */
+GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() {
+  this.result.messages = this.result.messages
+      .filter(
+          message => message.tag !== 'autogenerated:gerrit:deleteReviewer'
+      );
+};
+
+/**
+ * Is a part of _groupUpdates(). Creates a new batch of updates.
+ *
+ * @param {Object} update instance of ReviewerUpdateInfo
+ */
+GrReviewerUpdatesParser.prototype._startBatch = function(update) {
+  this._updateItems = [];
+  return {
+    author: update.updated_by,
+    date: update.updated,
+    type: 'REVIEWER_UPDATE',
+  };
+};
+
+/**
+ * Is a part of _groupUpdates(). Validates current batch:
+ * - filters out updates that don't change reviewer state.
+ * - updates current reviewer state.
+ *
+ * @param {Object} update instance of ReviewerUpdateInfo
+ */
+GrReviewerUpdatesParser.prototype._completeBatch = function(update) {
+  const items = [];
+  for (const accountId in this._updateItems) {
+    if (!this._updateItems.hasOwnProperty(accountId)) continue;
+    const updateItem = this._updateItems[accountId];
+    if (this._lastState[accountId] !== updateItem.state) {
+      this._lastState[accountId] = updateItem.state;
+      items.push(updateItem);
     }
-    const parser = new GrReviewerUpdatesParser(change);
-    parser._filterRemovedMessages();
-    parser._groupUpdates();
-    parser._formatUpdates();
-    parser._advanceUpdates();
-    return parser.result;
-  };
+  }
+  if (items.length) {
+    this._batch.updates = items;
+  }
+};
 
-  GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS = 500;
-  GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS = 6000;
-
-  GrReviewerUpdatesParser.prototype.result = null;
-  GrReviewerUpdatesParser.prototype._batch = null;
-  GrReviewerUpdatesParser.prototype._updateItems = null;
-  GrReviewerUpdatesParser.prototype._lastState = null;
-
-  /**
-   * Removes messages that describe removed reviewers, since reviewer_updates
-   * are used.
-   */
-  GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() {
-    this.result.messages = this.result.messages
-        .filter(
-            message => message.tag !== 'autogenerated:gerrit:deleteReviewer'
-        );
-  };
-
-  /**
-   * Is a part of _groupUpdates(). Creates a new batch of updates.
-   *
-   * @param {Object} update instance of ReviewerUpdateInfo
-   */
-  GrReviewerUpdatesParser.prototype._startBatch = function(update) {
-    this._updateItems = [];
-    return {
-      author: update.updated_by,
-      date: update.updated,
-      type: 'REVIEWER_UPDATE',
+/**
+ * Groups reviewer updates. Sequential updates are grouped if:
+ * - They were performed within short timeframe (6 seconds)
+ * - Made by the same person
+ * - Non-change updates are discarded within a group
+ * - Groups with no-change updates are discarded (eg CC -> CC)
+ */
+GrReviewerUpdatesParser.prototype._groupUpdates = function() {
+  const updates = this.result.reviewer_updates;
+  const newUpdates = updates.reduce((newUpdates, update) => {
+    if (!this._batch) {
+      this._batch = this._startBatch(update);
+    }
+    const updateDate = util.parseDate(update.updated).getTime();
+    const batchUpdateDate = util.parseDate(this._batch.date).getTime();
+    const reviewerId = update.reviewer._account_id.toString();
+    if (updateDate - batchUpdateDate >
+        GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS ||
+        update.updated_by._account_id !== this._batch.author._account_id) {
+      // Next sequential update should form new group.
+      this._completeBatch();
+      if (this._batch.updates && this._batch.updates.length) {
+        newUpdates.push(this._batch);
+      }
+      this._batch = this._startBatch(update);
+    }
+    this._updateItems[reviewerId] = {
+      reviewer: update.reviewer,
+      state: update.state,
     };
-  };
-
-  /**
-   * Is a part of _groupUpdates(). Validates current batch:
-   * - filters out updates that don't change reviewer state.
-   * - updates current reviewer state.
-   *
-   * @param {Object} update instance of ReviewerUpdateInfo
-   */
-  GrReviewerUpdatesParser.prototype._completeBatch = function(update) {
-    const items = [];
-    for (const accountId in this._updateItems) {
-      if (!this._updateItems.hasOwnProperty(accountId)) continue;
-      const updateItem = this._updateItems[accountId];
-      if (this._lastState[accountId] !== updateItem.state) {
-        this._lastState[accountId] = updateItem.state;
-        items.push(updateItem);
-      }
+    if (this._lastState[reviewerId]) {
+      this._updateItems[reviewerId].prev_state = this._lastState[reviewerId];
     }
-    if (items.length) {
-      this._batch.updates = items;
-    }
-  };
+    return newUpdates;
+  }, []);
+  this._completeBatch();
+  if (this._batch.updates && this._batch.updates.length) {
+    newUpdates.push(this._batch);
+  }
+  this.result.reviewer_updates = newUpdates;
+};
 
-  /**
-   * Groups reviewer updates. Sequential updates are grouped if:
-   * - They were performed within short timeframe (6 seconds)
-   * - Made by the same person
-   * - Non-change updates are discarded within a group
-   * - Groups with no-change updates are discarded (eg CC -> CC)
-   */
-  GrReviewerUpdatesParser.prototype._groupUpdates = function() {
-    const updates = this.result.reviewer_updates;
-    const newUpdates = updates.reduce((newUpdates, update) => {
-      if (!this._batch) {
-        this._batch = this._startBatch(update);
-      }
-      const updateDate = util.parseDate(update.updated).getTime();
-      const batchUpdateDate = util.parseDate(this._batch.date).getTime();
-      const reviewerId = update.reviewer._account_id.toString();
-      if (updateDate - batchUpdateDate >
-          GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS ||
-          update.updated_by._account_id !== this._batch.author._account_id) {
-        // Next sequential update should form new group.
-        this._completeBatch();
-        if (this._batch.updates && this._batch.updates.length) {
-          newUpdates.push(this._batch);
-        }
-        this._batch = this._startBatch(update);
-      }
-      this._updateItems[reviewerId] = {
-        reviewer: update.reviewer,
-        state: update.state,
-      };
-      if (this._lastState[reviewerId]) {
-        this._updateItems[reviewerId].prev_state = this._lastState[reviewerId];
-      }
-      return newUpdates;
-    }, []);
-    this._completeBatch();
-    if (this._batch.updates && this._batch.updates.length) {
-      newUpdates.push(this._batch);
-    }
-    this.result.reviewer_updates = newUpdates;
-  };
-
-  /**
-   * Generates update message for reviewer state change.
-   *
-   * @param {string} prev previous reviewer state.
-   * @param {string} state current reviewer state.
-   * @return {string}
-   */
-  GrReviewerUpdatesParser.prototype._getUpdateMessage = function(prev, state) {
-    if (prev === 'REMOVED' || !prev) {
-      return 'Added to ' + state.toLowerCase() + ': ';
-    } else if (state === 'REMOVED') {
-      if (prev) {
-        return 'Removed from ' + prev.toLowerCase() + ': ';
-      } else {
-        return 'Removed : ';
-      }
+/**
+ * Generates update message for reviewer state change.
+ *
+ * @param {string} prev previous reviewer state.
+ * @param {string} state current reviewer state.
+ * @return {string}
+ */
+GrReviewerUpdatesParser.prototype._getUpdateMessage = function(prev, state) {
+  if (prev === 'REMOVED' || !prev) {
+    return 'Added to ' + state.toLowerCase() + ': ';
+  } else if (state === 'REMOVED') {
+    if (prev) {
+      return 'Removed from ' + prev.toLowerCase() + ': ';
     } else {
-      return 'Moved from ' + prev.toLowerCase() + ' to ' + state.toLowerCase() +
-          ': ';
+      return 'Removed : ';
     }
-  };
+  } else {
+    return 'Moved from ' + prev.toLowerCase() + ' to ' + state.toLowerCase() +
+        ': ';
+  }
+};
 
-  /**
-   * Groups updates for same category (eg CC->CC) into a hash arrays of
-   * reviewers.
-   *
-   * @param {!Array<!Object>} updates Array of ReviewerUpdateItemInfo.
-   * @return {!Object} Hash of arrays of AccountInfo, message as key.
-   */
-  GrReviewerUpdatesParser.prototype._groupUpdatesByMessage = function(updates) {
-    return updates.reduce((result, item) => {
-      const message = this._getUpdateMessage(item.prev_state, item.state);
-      if (!result[message]) {
-        result[message] = [];
-      }
-      result[message].push(item.reviewer);
-      return result;
-    }, {});
-  };
-
-  /**
-   * Generates text messages for grouped reviewer updates.
-   * Formats reviewer updates to a (not yet implemented) EventInfo instance.
-   *
-   * @see https://gerrit-review.googlesource.com/c/94490/
-   */
-  GrReviewerUpdatesParser.prototype._formatUpdates = function() {
-    for (const update of this.result.reviewer_updates) {
-      const grouppedReviewers = this._groupUpdatesByMessage(update.updates);
-      const newUpdates = [];
-      for (const message in grouppedReviewers) {
-        if (grouppedReviewers.hasOwnProperty(message)) {
-          newUpdates.push({
-            message,
-            reviewers: grouppedReviewers[message],
-          });
-        }
-      }
-      update.updates = newUpdates;
+/**
+ * Groups updates for same category (eg CC->CC) into a hash arrays of
+ * reviewers.
+ *
+ * @param {!Array<!Object>} updates Array of ReviewerUpdateItemInfo.
+ * @return {!Object} Hash of arrays of AccountInfo, message as key.
+ */
+GrReviewerUpdatesParser.prototype._groupUpdatesByMessage = function(updates) {
+  return updates.reduce((result, item) => {
+    const message = this._getUpdateMessage(item.prev_state, item.state);
+    if (!result[message]) {
+      result[message] = [];
     }
-  };
+    result[message].push(item.reviewer);
+    return result;
+  }, {});
+};
 
-  /**
-   * Moves reviewer updates that are within short time frame of change messages
-   * back in time so they would come before change messages.
-   * TODO(viktard): Remove when server-side serves reviewer updates like so.
-   */
-  GrReviewerUpdatesParser.prototype._advanceUpdates = function() {
-    const updates = this.result.reviewer_updates;
-    const messages = this.result.messages;
-    messages.forEach((message, index) => {
-      const messageDate = util.parseDate(message.date).getTime();
-      const nextMessageDate = index === messages.length - 1 ? null :
-        util.parseDate(messages[index + 1].date).getTime();
-      for (const update of updates) {
-        const date = util.parseDate(update.date).getTime();
-        if (date >= messageDate &&
-            (!nextMessageDate || date < nextMessageDate)) {
-          const timestamp = util.parseDate(update.date).getTime() -
-              GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS;
-          update.date = new Date(timestamp)
-              .toISOString()
-              .replace('T', ' ')
-              .replace('Z', '000000');
-        }
-        if (nextMessageDate && date > nextMessageDate) {
-          break;
-        }
+/**
+ * Generates text messages for grouped reviewer updates.
+ * Formats reviewer updates to a (not yet implemented) EventInfo instance.
+ *
+ * @see https://gerrit-review.googlesource.com/c/94490/
+ */
+GrReviewerUpdatesParser.prototype._formatUpdates = function() {
+  for (const update of this.result.reviewer_updates) {
+    const grouppedReviewers = this._groupUpdatesByMessage(update.updates);
+    const newUpdates = [];
+    for (const message in grouppedReviewers) {
+      if (grouppedReviewers.hasOwnProperty(message)) {
+        newUpdates.push({
+          message,
+          reviewers: grouppedReviewers[message],
+        });
       }
-    });
-  };
+    }
+    update.updates = newUpdates;
+  }
+};
 
-  window.GrReviewerUpdatesParser = GrReviewerUpdatesParser;
-})(window);
+/**
+ * Moves reviewer updates that are within short time frame of change messages
+ * back in time so they would come before change messages.
+ * TODO(viktard): Remove when server-side serves reviewer updates like so.
+ */
+GrReviewerUpdatesParser.prototype._advanceUpdates = function() {
+  const updates = this.result.reviewer_updates;
+  const messages = this.result.messages;
+  messages.forEach((message, index) => {
+    const messageDate = util.parseDate(message.date).getTime();
+    const nextMessageDate = index === messages.length - 1 ? null :
+      util.parseDate(messages[index + 1].date).getTime();
+    for (const update of updates) {
+      const date = util.parseDate(update.date).getTime();
+      if (date >= messageDate &&
+          (!nextMessageDate || date < nextMessageDate)) {
+        const timestamp = util.parseDate(update.date).getTime() -
+            GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS;
+        update.date = new Date(timestamp)
+            .toISOString()
+            .replace('T', ' ')
+            .replace('Z', '000000');
+      }
+      if (nextMessageDate && date > nextMessageDate) {
+        break;
+      }
+    }
+  });
+};
+
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index 4e17e13..5821398 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -26,7 +26,8 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../../../scripts/util.js';
-import './gr-reviewer-updates-parser.js';
+import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
+
 suite('gr-reviewer-updates-parser tests', () => {
   let sandbox;
   let instance;
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
index b6ab6a7..f51976d 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
@@ -16,102 +16,92 @@
  */
 import {GrDisplayNameUtils} from '../gr-display-name-utils/gr-display-name-utils.js';
 
-(function(window) {
-  'use strict';
-  window.Gerrit = window.Gerrit || {};
+window.Gerrit = window.Gerrit || {};
+/**
+ * @enum {string}
+ */
+Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES = {
+  REVIEWER: 'reviewers',
+  CC: 'ccs',
+  ANY: 'any',
+};
 
-  if (window.GrReviewerSuggestionsProvider) {
-    return;
+export class GrReviewerSuggestionsProvider {
+  static create(restApi, changeNumber, usersType) {
+    switch (usersType) {
+      case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER:
+        return new GrReviewerSuggestionsProvider(restApi, changeNumber,
+            input => restApi.getChangeSuggestedReviewers(changeNumber,
+                input));
+      case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC:
+        return new GrReviewerSuggestionsProvider(restApi, changeNumber,
+            input => restApi.getChangeSuggestedCCs(changeNumber, input));
+      case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY:
+        return new GrReviewerSuggestionsProvider(restApi, changeNumber,
+            input => restApi.getSuggestedAccounts(
+                `cansee:${changeNumber} ${input}`));
+      default:
+        throw new Error(`Unknown users type: ${usersType}`);
+    }
   }
 
-  /**
-   * @enum {string}
-   */
-  Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES = {
-    REVIEWER: 'reviewers',
-    CC: 'ccs',
-    ANY: 'any',
-  };
+  constructor(restAPI, changeNumber, apiCall) {
+    this._changeNumber = changeNumber;
+    this._apiCall = apiCall;
+    this._restAPI = restAPI;
+  }
 
-  class GrReviewerSuggestionsProvider {
-    static create(restApi, changeNumber, usersType) {
-      switch (usersType) {
-        case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER:
-          return new GrReviewerSuggestionsProvider(restApi, changeNumber,
-              input => restApi.getChangeSuggestedReviewers(changeNumber,
-                  input));
-        case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC:
-          return new GrReviewerSuggestionsProvider(restApi, changeNumber,
-              input => restApi.getChangeSuggestedCCs(changeNumber, input));
-        case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY:
-          return new GrReviewerSuggestionsProvider(restApi, changeNumber,
-              input => restApi.getSuggestedAccounts(
-                  `cansee:${changeNumber} ${input}`));
-        default:
-          throw new Error(`Unknown users type: ${usersType}`);
-      }
-    }
-
-    constructor(restAPI, changeNumber, apiCall) {
-      this._changeNumber = changeNumber;
-      this._apiCall = apiCall;
-      this._restAPI = restAPI;
-    }
-
-    init() {
-      if (this._initPromise) {
-        return this._initPromise;
-      }
-      const getConfigPromise = this._restAPI.getConfig().then(cfg => {
-        this._config = cfg;
-      });
-      const getLoggedInPromise = this._restAPI.getLoggedIn().then(loggedIn => {
-        this._loggedIn = loggedIn;
-      });
-      this._initPromise = Promise.all([getConfigPromise, getLoggedInPromise])
-          .then(() => {
-            this._initialized = true;
-          });
+  init() {
+    if (this._initPromise) {
       return this._initPromise;
     }
-
-    getSuggestions(input) {
-      if (!this._initialized || !this._loggedIn) {
-        return Promise.resolve([]);
-      }
-
-      return this._apiCall(input)
-          .then(reviewers => (reviewers || []));
-    }
-
-    makeSuggestionItem(suggestion) {
-      if (suggestion.account) {
-        // Reviewer is an account suggestion from getChangeSuggestedReviewers.
-        return {
-          name: GrDisplayNameUtils.getAccountDisplayName(this._config,
-              suggestion.account),
-          value: suggestion,
-        };
-      }
-
-      if (suggestion.group) {
-        // Reviewer is a group suggestion from getChangeSuggestedReviewers.
-        return {
-          name: GrDisplayNameUtils.getGroupDisplayName(suggestion.group),
-          value: suggestion,
-        };
-      }
-
-      if (suggestion._account_id) {
-        // Reviewer is an account suggestion from getSuggestedAccounts.
-        return {
-          name: GrDisplayNameUtils.getAccountDisplayName(this._config,
-              suggestion),
-          value: {account: suggestion, count: 1},
-        };
-      }
-    }
+    const getConfigPromise = this._restAPI.getConfig().then(cfg => {
+      this._config = cfg;
+    });
+    const getLoggedInPromise = this._restAPI.getLoggedIn().then(loggedIn => {
+      this._loggedIn = loggedIn;
+    });
+    this._initPromise = Promise.all([getConfigPromise, getLoggedInPromise])
+        .then(() => {
+          this._initialized = true;
+        });
+    return this._initPromise;
   }
 
-  window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
-})(window);
+  getSuggestions(input) {
+    if (!this._initialized || !this._loggedIn) {
+      return Promise.resolve([]);
+    }
+
+    return this._apiCall(input)
+        .then(reviewers => (reviewers || []));
+  }
+
+  makeSuggestionItem(suggestion) {
+    if (suggestion.account) {
+      // Reviewer is an account suggestion from getChangeSuggestedReviewers.
+      return {
+        name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+            suggestion.account),
+        value: suggestion,
+      };
+    }
+
+    if (suggestion.group) {
+      // Reviewer is a group suggestion from getChangeSuggestedReviewers.
+      return {
+        name: GrDisplayNameUtils.getGroupDisplayName(suggestion.group),
+        value: suggestion,
+      };
+    }
+
+    if (suggestion._account_id) {
+      // Reviewer is an account suggestion from getSuggestedAccounts.
+      return {
+        name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+            suggestion),
+        value: {account: suggestion, count: 1},
+      };
+    }
+  }
+}
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
index f7308e3..a9ad50c 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
@@ -33,8 +33,8 @@
 <script type="module">
 import '../../test/common-test-setup.js';
 import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import './gr-reviewer-suggestions-provider.js';
 import {GrDisplayNameUtils} from '../gr-display-name-utils/gr-display-name-utils.js';
+import {GrReviewerSuggestionsProvider} from './gr-reviewer-suggestions-provider.js';
 
 suite('GrReviewerSuggestionsProvider tests', () => {
   let sandbox;