Convert the delete-project plugin to TypeScript

This is the first upstream plugin to be converted to TypeScript. So
this is also going to be used as a blueprint for other conversions.

The plugin now depends on the new npm package
@gerritcodereview/typescript-api.

The tsconfig, rollup.config, eslint configs, and npm modules are defined
in the top-level plugins/ directory. It should be possible for most of
the TypeScript plugins to use the same configs and npm modules.

Also converts from Polymer to Lit. Note that Lit is compiled into the
plugin, so it does not depend on Polymer or Lit being present in the
Gerrit core bundle. The compresssed size of the plugin is still just 8k.

Linting works.

There are not TypeScript tests yet, so this is not a blueprint for
writing and executing tests in a TypeScript based plugin.

Change-Id: I919edd722befedf3d24db2760ababa7b25dcbddb
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index e69de29..0000000
--- a/.eslintignore
+++ /dev/null
diff --git a/gr-delete-repo/plugin.js b/.eslintrc.js
similarity index 74%
copy from gr-delete-repo/plugin.js
copy to .eslintrc.js
index 2e22d59..30df322 100644
--- a/gr-delete-repo/plugin.js
+++ b/.eslintrc.js
@@ -1,6 +1,6 @@
 /**
  * @license
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,9 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import './gr-delete-repo.js';
-
-Gerrit.install(plugin => {
-  plugin.registerCustomComponent(
-      'repo-command', 'gr-delete-repo');
-});
+__plugindir = 'delete-project';
+module.exports = {
+  extends: '../.eslintrc.js',
+};
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 6d9ae7c..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,166 +0,0 @@
-{
-  "extends": ["eslint:recommended", "google"],
-  "parserOptions": {
-    "ecmaVersion": 8,
-    "sourceType": "module"
-  },
-  "env": {
-    "browser": true,
-    "es6": true
-  },
-  "globals": {
-    "__dirname": false,
-    "app": false,
-    "page": false,
-    "Polymer": false,
-    "process": false,
-    "require": false,
-    "Gerrit": false,
-    "Promise": false,
-    "assert": false,
-    "test": false,
-    "flushAsynchronousOperations": false
-  },
-  "rules": {
-    "arrow-parens": ["error", "as-needed"],
-    "block-spacing": ["error", "always"],
-    "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
-    "camelcase": "off",
-    "comma-dangle": ["error", {
-      "arrays": "always-multiline",
-      "objects": "always-multiline",
-      "imports": "always-multiline",
-      "exports": "always-multiline",
-      "functions": "never"
-    }],
-    "eol-last": "off",
-    "indent": ["error", 2, {
-      "MemberExpression": 2,
-      "FunctionDeclaration": {"body": 1, "parameters": 2},
-      "FunctionExpression": {"body": 1, "parameters": 2},
-      "CallExpression": {"arguments": 2 },
-      "ArrayExpression": 1,
-      "ObjectExpression": 1,
-      "SwitchCase": 1
-    }],
-    "keyword-spacing": ["error", { "after": true, "before": true }],
-    "lines-between-class-members": ["error", "always"],
-    "max-len": [
-      "error",
-      80,
-      2,
-      {
-        "ignoreComments": true,
-        "ignorePattern": "^import .*;$"
-      }
-    ],
-    "new-cap": ["error", { "capIsNewExceptions": ["Polymer", "LegacyElementMixin", "GestureEventListeners", "LegacyDataMixin"] }],
-    "no-console": "off",
-    "no-multiple-empty-lines": [ "error", { "max": 1 } ],
-    "no-prototype-builtins": "off",
-    "no-redeclare": "off",
-    "no-restricted-syntax": [
-      "error",
-      {
-        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='test'][property.name='only']",
-        "message": "Remove test.only."
-      },
-      {
-        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='suite'][property.name='only']",
-        "message": "Remove suite.only."
-      }
-    ],
-    "no-undef": "off",
-    "no-useless-escape": "off",
-    "no-var": "error",
-    "object-shorthand": ["error", "always"],
-    "padding-line-between-statements": [
-      "error",
-      {
-        "blankLine": "always",
-        "prev": "class",
-        "next": "*"
-      },
-      {
-        "blankLine": "always",
-        "prev": "*",
-        "next": "class"
-      }
-    ],
-    "prefer-arrow-callback": "error",
-    "prefer-const": "error",
-    "prefer-spread": "error",
-    "quote-props": ["error", "consistent-as-needed"],
-    "semi": [2, "always"],
-    "template-curly-spacing": "error",
-    "valid-jsdoc": "off",
-
-    "require-jsdoc": 0,
-    "valid-jsdoc": 0,
-    "jsdoc/check-alignment": 2,
-    "jsdoc/check-examples": 0,
-    "jsdoc/check-indentation": 0,
-    "jsdoc/check-param-names": 0,
-    "jsdoc/check-syntax": 0,
-    "jsdoc/check-tag-names": 0,
-    "jsdoc/check-types": 0,
-    "jsdoc/implements-on-classes": 2,
-    "jsdoc/match-description": 0,
-    "jsdoc/newline-after-description": 2,
-    "jsdoc/no-types": 0,
-    "jsdoc/no-undefined-types": 0,
-    "jsdoc/require-description": 0,
-    "jsdoc/require-description-complete-sentence": 0,
-    "jsdoc/require-example": 0,
-    "jsdoc/require-hyphen-before-param-description": 0,
-    "jsdoc/require-jsdoc": 0,
-    "jsdoc/require-param": 0,
-    "jsdoc/require-param-description": 0,
-    "jsdoc/require-param-name": 2,
-    "jsdoc/require-param-type": 2,
-    "jsdoc/require-returns": 0,
-    "jsdoc/require-returns-check": 0,
-    "jsdoc/require-returns-description": 0,
-    "jsdoc/require-returns-type": 2,
-    "jsdoc/valid-types": 2,
-    "jsdoc/require-file-overview": ["error", {
-      "tags": {
-        "license": {
-          "mustExist": true,
-          "preventDuplicates": true
-        }
-      }
-    }],
-    "import/named": 2,
-    "import/no-unresolved": 2,
-    "import/no-self-import": 2,
-    // The no-cycle rule is slow, because it doesn't cache dependencies.
-    // Disable it.
-    "import/no-cycle": 0,
-    "import/no-useless-path-segments": 2,
-    "import/no-unused-modules": 2,
-    "import/no-default-export": 2
-  },
-  "plugins": [
-    "html",
-    "jsdoc",
-    "import"
-  ],
-  "settings": {
-    "html/report-bad-indent": "error"
-  },
-  "overrides": [
-    {
-      "files": ["*_html.js", "*-styles.js", "externs.js"],
-      "rules": {
-        "max-len": "off"
-      }
-    },
-    {
-      "files": ["*.html"],
-      "rules": {
-        "jsdoc/require-file-overview": "off"
-      }
-    }
-  ]
-}
diff --git a/.gitignore b/.gitignore
index 4c0afce..81322bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
 /.settings
 /bazel-*
 /eclipse-out
+/node_modules
diff --git a/BUILD b/BUILD
index fd820e4..47d5db0 100644
--- a/BUILD
+++ b/BUILD
@@ -8,6 +8,7 @@
     "gerrit_plugin",
 )
 load("//tools/bzl:js.bzl", "gerrit_js_bundle")
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
 
 gerrit_plugin(
     name = "delete-project",
@@ -23,9 +24,31 @@
     deps = ["@commons-io//jar"],
 )
 
+ts_config(
+    name = "tsconfig",
+    src = "tsconfig.json",
+    deps = [
+        "//plugins:tsconfig-plugins-base.json",
+    ],
+)
+
+ts_project(
+    name = "gr-delete-repo-ts",
+    srcs = glob([
+        "gr-delete-repo/**/*.ts",
+    ]),
+    incremental = True,
+    tsc = "//tools/node_tools:tsc-bin",
+    tsconfig = ":tsconfig",
+    deps = [
+        "@plugins_npm//@gerritcodereview/typescript-api",
+        "@plugins_npm//lit",
+    ],
+)
+
 gerrit_js_bundle(
     name = "gr-delete-repo",
-    srcs = glob(["gr-delete-repo/*.js"]),
+    srcs = [":gr-delete-repo-ts"],
     entry_point = "gr-delete-repo/plugin.js",
 )
 
@@ -49,23 +72,27 @@
     ],
 )
 
-# Define the eslinter for the plugin
-# The eslint macro creates 2 rules: lint_test and lint_bin
+# The eslint macro creates 2 rules: lint_test and lint_bin. Typical usage:
+# bazel test $DIR:lint_test
+# bazel run $DIR:lint_bin -- --fix $PATH_TO_SRCS
 eslint(
     name = "lint",
-    srcs = glob([
-        "gr-delete-repo/**/*.js",
-    ]),
-    config = ".eslintrc.json",
-    data = [],
-    extensions = [
-        ".js",
+    srcs = glob(["gr-delete-repo/**/*"]),
+    config = ".eslintrc.js",
+    data = [
+        "tsconfig.json",
+        "//plugins:.eslintrc.js",
+        "//plugins:.prettierrc.js",
+        "//plugins:tsconfig-plugins-base.json",
     ],
-    ignore = ".eslintignore",
+    extensions = [".ts"],
+    ignore = "//plugins:.eslintignore",
     plugins = [
         "@npm//eslint-config-google",
         "@npm//eslint-plugin-html",
         "@npm//eslint-plugin-import",
         "@npm//eslint-plugin-jsdoc",
+        "@npm//eslint-plugin-prettier",
+        "@npm//gts",
     ],
 )
diff --git a/gr-delete-repo/gr-delete-repo.js b/gr-delete-repo/gr-delete-repo.js
deleted file mode 100644
index 20c3555..0000000
--- a/gr-delete-repo/gr-delete-repo.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {htmlTemplate} from './gr-delete-repo_html.js';
-
-class GrDeleteRepo extends Polymer.Element {
-  /** @returns {string} name of the component */
-  static get is() { return 'gr-delete-repo'; }
-
-  /** @returns {?} template for this component */
-  static get template() { return htmlTemplate; }
-
-  /**
-   * Defines properties of the component
-   *
-   * @returns {?}
-   */
-  static get properties() {
-    return {
-      repoName: String,
-      config: Object,
-      action: Object,
-      actionId: String,
-    };
-  }
-
-  connectedCallback() {
-    super.connectedCallback();
-    this.actionId = this.plugin.getPluginName() + '~delete';
-    this.action = this.config.actions[this.actionId];
-    this.hidden = !this.action;
-  }
-
-  _handleCommandTap() {
-    this.$.deleteRepoOverlay.open();
-  }
-
-  _handleCloseDeleteRepo() {
-    this.$.deleteRepoOverlay.close();
-  }
-
-  _handleDeleteRepo() {
-    const endpoint = '/projects/' +
-        encodeURIComponent(this.repoName) + '/' +
-        this.actionId;
-
-    const json = {
-      force: this.$.forceDeleteOpenChangesCheckBox.checked,
-      preserve: this.$.preserveGitRepoCheckBox.checked,
-    };
-
-    const errFn = response => {
-      this.dispatchEvent(new CustomEvent('page-error', {
-        detail: {response},
-        bubbles: true,
-        composed: true,
-      }));
-    };
-
-    return this.plugin.restApi().send(
-        this.action.method, endpoint, json, errFn)
-        .then(r => {
-          this.plugin.restApi().invalidateReposCache();
-          Gerrit.Nav.navigateToRelativeUrl('/admin/repos');
-        });
-  }
-}
-
-customElements.define(GrDeleteRepo.is, GrDeleteRepo);
diff --git a/gr-delete-repo/gr-delete-repo.ts b/gr-delete-repo/gr-delete-repo.ts
new file mode 100644
index 0000000..e16cefb
--- /dev/null
+++ b/gr-delete-repo/gr-delete-repo.ts
@@ -0,0 +1,173 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
+import {
+  ActionInfo,
+  ConfigInfo,
+  RepoName,
+} from '@gerritcodereview/typescript-api/rest-api';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+
+// TODO: This should be defined and exposed by @gerritcodereview/typescript-api
+type GrOverlay = Element & {
+  open(): void;
+  close(): void;
+};
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-delete-repo': GrDeleteRepo;
+  }
+}
+
+@customElement('gr-delete-repo')
+class GrDeleteRepo extends LitElement {
+  @query('#deleteRepoOverlay')
+  deleteRepoOverlay?: GrOverlay;
+
+  @query('#preserveGitRepoCheckBox')
+  preserveGitRepoCheckBox?: HTMLInputElement;
+
+  @query('#forceDeleteOpenChangesCheckBox')
+  forceDeleteOpenChangesCheckBox?: HTMLInputElement;
+
+  /** Guaranteed to be provided by the 'repo-command' endpoint. */
+  @property({type: Object})
+  plugin!: PluginApi;
+
+  /** Guaranteed to be provided by the 'repo-command' endpoint. */
+  @property({type: Object})
+  config!: ConfigInfo;
+
+  /** Guaranteed to be provided by the 'repo-command' endpoint. */
+  @property({type: String})
+  repoName!: RepoName;
+
+  @state()
+  private error?: string;
+
+  static styles = css`
+    :host {
+      display: block;
+      margin-bottom: var(--spacing-xxl);
+    }
+    /* TODO: Find a way to use shared styles in lit elements in plugins. */
+    h3 {
+      font: inherit;
+      margin: 0;
+    }
+    .error {
+      color: red;
+    }
+  `;
+
+  get action(): ActionInfo | undefined {
+    return this.config.actions?.[this.actionId];
+  }
+
+  get actionId(): string {
+    return `${this.plugin.getPluginName()}~delete`;
+  }
+
+  private renderError() {
+    if (!this.error) return;
+    return html`<div class="error">${this.error}</div>`;
+  }
+
+  protected render() {
+    if (!this.action) return;
+    return html`
+      <h3>${this.action.label}</h3>
+      <gr-button
+        title="${this.action.title}"
+        ?disabled="${!this.action.enabled}"
+        @click="${() => {
+          this.error = undefined;
+          this.deleteRepoOverlay?.open();
+        }}"
+      >
+        ${this.action.label}
+      </gr-button>
+      ${this.renderError()}
+      <gr-overlay id="deleteRepoOverlay" with-backdrop>
+        <gr-dialog
+          id="deleteRepoDialog"
+          confirm-label="Delete"
+          @confirm="${this.deleteRepo}"
+          @cancel="${() => this.deleteRepoOverlay?.close()}"
+        >
+          <div class="header" slot="header">
+            Are you really sure you want to delete the repo: "${this.repoName}"?
+          </div>
+          <div class="main" slot="main">
+            <div>
+              <div id="form">
+                <section>
+                  <input type="checkbox" id="forceDeleteOpenChangesCheckBox" />
+                  <label for="forceDeleteOpenChangesCheckBox"
+                    >Delete repo even if open changes exist?</label
+                  >
+                </section>
+                <section>
+                  <input type="checkbox" id="preserveGitRepoCheckBox" />
+                  <label for="preserveGitRepoCheckBox"
+                    >Preserve GIT Repository?</label
+                  >
+                </section>
+              </div>
+            </div>
+          </div>
+        </gr-dialog>
+      </gr-overlay>
+    `;
+  }
+
+  private deleteRepo() {
+    if (!this.action) {
+      this.error = 'delete action undefined';
+      this.deleteRepoOverlay?.close();
+      return;
+    }
+    if (!this.action.method) {
+      this.error = 'delete action does not have a HTTP method set';
+      this.deleteRepoOverlay?.close();
+      return;
+    }
+    this.error = undefined;
+
+    const endpoint = `/projects/${encodeURIComponent(this.repoName)}/${
+      this.actionId
+    }`;
+    const json = {
+      force: this.forceDeleteOpenChangesCheckBox?.checked ?? false,
+      preserve: this.preserveGitRepoCheckBox?.checked ?? false,
+    };
+    return this.plugin
+      .restApi()
+      .send(this.action.method, endpoint, json)
+      .then(_ => {
+        this.plugin.restApi().invalidateReposCache();
+        this.deleteRepoOverlay?.close();
+        window.location.href = '/admin/repos';
+      })
+      .catch(e => {
+        this.error = e;
+        this.deleteRepoOverlay?.close();
+      });
+  }
+}
diff --git a/gr-delete-repo/gr-delete-repo_html.js b/gr-delete-repo/gr-delete-repo_html.js
deleted file mode 100644
index e3a985d..0000000
--- a/gr-delete-repo/gr-delete-repo_html.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export const htmlTemplate = Polymer.html`
-    <style include="shared-styles"></style>
-    <style include="gr-form-styles">
-    :host {
-      display: block;
-      margin-bottom: var(--spacing-xxl);
-    }
-    </style>
-    <h3 class="heading-3">
-      [[action.label]]
-    </h3>
-    <gr-button
-      title="[[action.title]]"
-      loading="[[_deleting]]"
-      disabled="[[!action.enabled]]"
-      on-click="_handleCommandTap"
-    >
-      [[action.label]]
-    </gr-button>
-    <gr-overlay id="deleteRepoOverlay" with-backdrop>
-      <gr-dialog
-          id="deleteRepoDialog"
-          confirm-label="Delete"
-          on-confirm="_handleDeleteRepo"
-          on-cancel="_handleCloseDeleteRepo">
-        <div class="header" slot="header">
-          Are you really sure you want to delete the repo: "[[repoName]]"?
-        </div>
-        <div class="main" slot="main">
-          <div class="gr-form-styles">
-            <div id="form">
-                <section>
-                  <input
-                      type="checkbox"
-                      id="forceDeleteOpenChangesCheckBox">
-                  <label for="forceDeleteOpenChangesCheckBox">Delete repo even if open changes exist?</label>
-                </section>
-                <section>
-                  <input
-                      type="checkbox"
-                      id="preserveGitRepoCheckBox">
-                  <label for="preserveGitRepoCheckBox">Preserve GIT Repository?</label>
-                </section>
-            </div>
-          </div>
-        </div>
-      </gr-dialog>
-    </gr-overlay>
-`;
diff --git a/gr-delete-repo/plugin.js b/gr-delete-repo/plugin.ts
similarity index 77%
rename from gr-delete-repo/plugin.js
rename to gr-delete-repo/plugin.ts
index 2e22d59..7355b50 100644
--- a/gr-delete-repo/plugin.js
+++ b/gr-delete-repo/plugin.ts
@@ -14,9 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import './gr-delete-repo.js';
+import '@gerritcodereview/typescript-api/gerrit';
+import './gr-delete-repo';
 
-Gerrit.install(plugin => {
-  plugin.registerCustomComponent(
-      'repo-command', 'gr-delete-repo');
+window.Gerrit.install(plugin => {
+  plugin.registerCustomComponent('repo-command', 'gr-delete-repo');
 });
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..9be4292
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,6 @@
+{
+  "extends": "../tsconfig-plugins-base.json",
+  "include": [
+    "gr-delete-repo/**/*",
+  ]
+}