Add polymer template checker

For details how to use it see the polygerrit-ui/README file.

Twinkie patch note: the template checker uses twinkie library for
checking. The library has a bug, that stops checker from working. As a
temporary workaround, the patch is added directly to this change. The
patch will be removed after fixing bug in twinkie library.

Change-Id: I3375d6654478e12eeb1df9a2b912125f6929838c
diff --git a/WORKSPACE b/WORKSPACE
index 428a6a4..a6a4937 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -70,6 +70,19 @@
     urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.5.0/rules_nodejs-3.5.0.tar.gz"],
 )
 
+http_archive(
+    name = "rules_pkg",
+    sha256 = "038f1caa773a7e35b3663865ffb003169c6a71dc995e39bf4815792f385d837d",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.4.0/rules_pkg-0.4.0.tar.gz",
+        "https://github.com/bazelbuild/rules_pkg/releases/download/0.4.0/rules_pkg-0.4.0.tar.gz",
+    ],
+)
+
+load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies")
+
+rules_pkg_dependencies()
+
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
@@ -941,6 +954,7 @@
 
 yarn_install(
     name = "npm",
+    data = ["//:twinkie.patch"],
     frozen_lockfile = False,
     package_json = "//:package.json",
     yarn_lock = "//:yarn.lock",
diff --git a/package.json b/package.json
index a3329a1..151b784 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,8 @@
   "dependencies": {
     "@bazel/rollup": "^3.5.0",
     "@bazel/terser": "^3.5.0",
-    "@bazel/typescript": "^3.5.0"
+    "@bazel/typescript": "^3.5.0",
+    "twinkie": "^1.1.2"
   },
   "devDependencies": {
     "@typescript-eslint/eslint-plugin": "^4.22.0",
@@ -34,7 +35,10 @@
     "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
     "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test",
     "test:debug": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --browsers ChromeDev --no-single-run --testFiles",
-    "test:single": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --testFiles"
+    "test:single": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --testFiles",
+    "postinstall": "(git apply --reverse --ignore-whitespace twinkie.patch || true) && git apply --ignore-whitespace twinkie.patch",
+    "polytest": "npm run safe_bazelisk test //polygerrit-ui/app:validate_polymer_templates",
+    "polytest:dev": "rm -rf ./polygerrit-ui/app/tmpl_out && npm run safe_bazelisk build //polygerrit-ui/app:template_test_tar && mkdir ./polygerrit-ui/app/tmpl_out && tar -xf bazel-bin/polygerrit-ui/app/template_test_tar.tar -C ./polygerrit-ui/app/tmpl_out"
   },
   "repository": {
     "type": "git",
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index c6e6cd9..0297324 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -228,6 +228,56 @@
 the "Before launch" section for IntelliJ. This is a temporary problem until
 typescript migration is complete.
 
+## Running Templates Test
+The templates test validates polymer templates. The test convert polymer
+templates into a plain typescript code and then run TS compiler. The test fails
+if TS compiler reports errors; in this case you will see TS errors in
+the log/output. Gerrit-CI automatically runs templates test.
+
+**Note**: Files defined in `ignore_templates_list` (`polygerrit-ui/app/BUILD`)
+are excluded from code generation and checking. If you don't know how to fix
+a problem, you can add a problematic template in the list.
+
+* To run test locally, use npm command:
+``` sh
+npm run polytest
+```
+
+* Often, the output from the previous command is not clear (cryptic TS errors).
+In this case, run the command
+```sh
+npm run polytest:dev
+```
+This command (re)creates the `polygerrit-ui/app/tmpl_out` directory and put
+generated files into it. For each polygerrit .ts file there is a generated file
+in the `tmpl_out` directory. If an original file doesn't contain a polymer
+template, the generated file is empty.
+
+You can open a problematic file in IDE and fix the problem. Ensure, that IDE
+uses `polygerrit-ui/app/tsconfig.json` as a project (usually, this is default).
+
+### Generated file overview
+
+A generated file starts with imports followed by a static content with
+different type definitions. You can skip this part - it doesn't contains
+anything usefule.
+
+After the static content there is a class definition. Example:
+```typescript
+export class GrCreateGroupDialogCheck extends GrCreateGroupDialog {
+  templateCheck() {
+    // Converted template
+    // Each HTML element from the template is wrapped into own block.
+  }
+}
+```
+
+The converted template usually quite straightforward, but in some cases
+additional functions are added. For example, `<element x=[[y.a]]>` converts into
+`el.x = y!.a` if y is a simple type. However, if y has a union type, like - `y:A|B`,
+then the generated code looks like `el.x=__f(y)!.a` (`y!.a` may result in a TS error
+if `a` is defined only in one type of a union). 
+
 ## Style guide
 
 We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
diff --git a/polygerrit-ui/app/.eslintignore b/polygerrit-ui/app/.eslintignore
index 087a049..bb30f23 100644
--- a/polygerrit-ui/app/.eslintignore
+++ b/polygerrit-ui/app/.eslintignore
@@ -2,3 +2,4 @@
 **/rollup.config.js
 node_modules_licenses
 !.eslintrc-bazel.js
+tmpl_out
diff --git a/polygerrit-ui/app/.gitignore b/polygerrit-ui/app/.gitignore
index c235144..6b96e60 100644
--- a/polygerrit-ui/app/.gitignore
+++ b/polygerrit-ui/app/.gitignore
@@ -1,2 +1,3 @@
 /plugins/
 /node_modules/
+/tmpl_out/
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 2f83182..c794b01 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -1,5 +1,7 @@
 load(":rules.bzl", "compile_ts", "polygerrit_bundle")
 load("//tools/js:eslint.bzl", "eslint")
+load("//tools/js:template_checker.bzl", "transform_polymer_templates")
+load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary", "nodejs_test", "npm_package_bin")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -48,15 +50,176 @@
         exclude = [
             "node_modules/**",
             "node_modules_licenses/**",
-            "template_test_srcs/**",
+            "tmpl_out/**",  # This directory is created by template checker in dev-mode
             "rollup.config.js",
         ],
     ),
-    include_tests = True,
+    additional_deps = [
+        "@ui_dev_npm//:node_modules",
+        "tsconfig_bazel.json",
+    ],
     # The same outdir also appears in the following files:
     # wct_test.sh
     # karma.conf.js
     ts_outdir = "_pg_with_tests_out",
+    ts_project = "tsconfig_bazel_test.json",
+)
+
+# Template checker reports problems in the following files. Ignore the files,
+# so template tests pass.
+# TODO: fix problems reported by template checker in these files.
+ignore_templates_list = [
+    "elements/admin/gr-access-section/gr-access-section_html.ts",
+    "elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts",
+    "elements/admin/gr-admin-view/gr-admin-view_html.ts",
+    "elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.ts",
+    "elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts",
+    "elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts",
+    "elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts",
+    "elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts",
+    "elements/admin/gr-group-members/gr-group-members_html.ts",
+    "elements/admin/gr-group/gr-group_html.ts",
+    "elements/admin/gr-permission/gr-permission_html.ts",
+    "elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.ts",
+    "elements/admin/gr-plugin-list/gr-plugin-list_html.ts",
+    "elements/admin/gr-repo-access/gr-repo-access_html.ts",
+    "elements/admin/gr-repo-commands/gr-repo-commands_html.ts",
+    "elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.ts",
+    "elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts",
+    "elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts",
+    "elements/admin/gr-repo/gr-repo_html.ts",
+    "elements/admin/gr-rule-editor/gr-rule-editor_html.ts",
+    "elements/change-list/gr-change-list-item/gr-change-list-item_html.ts",
+    "elements/change-list/gr-change-list-view/gr-change-list-view_html.ts",
+    "elements/change-list/gr-change-list/gr-change-list_html.ts",
+    "elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts",
+    "elements/change-list/gr-user-header/gr-user-header_html.ts",
+    "elements/change/gr-change-actions/gr-change-actions_html.ts",
+    "elements/change/gr-change-metadata/gr-change-metadata_html.ts",
+    "elements/change/gr-change-requirements/gr-change-requirements_html.ts",
+    "elements/change/gr-change-view/gr-change-view_html.ts",
+    "elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.ts",
+    "elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts",
+    "elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts",
+    "elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts",
+    "elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts",
+    "elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts",
+    "elements/change/gr-download-dialog/gr-download-dialog_html.ts",
+    "elements/change/gr-file-list-header/gr-file-list-header_html.ts",
+    "elements/change/gr-file-list/gr-file-list_html.ts",
+    "elements/change/gr-label-score-row/gr-label-score-row_html.ts",
+    "elements/change/gr-message/gr-message_html.ts",
+    "elements/change/gr-messages-list/gr-messages-list_html.ts",
+    "elements/change/gr-reply-dialog/gr-reply-dialog_html.ts",
+    "elements/change/gr-reviewer-list/gr-reviewer-list_html.ts",
+    "elements/change/gr-thread-list/gr-thread-list_html.ts",
+    "elements/checks/gr-hovercard-run_html.ts",
+    "elements/core/gr-main-header/gr-main-header_html.ts",
+    "elements/core/gr-search-bar/gr-search-bar_html.ts",
+    "elements/core/gr-smart-search/gr-smart-search_html.ts",
+    "elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts",
+    "elements/diff/gr-diff-builder/gr-diff-builder-element_html.ts",
+    "elements/diff/gr-diff-host/gr-diff-host_html.ts",
+    "elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts",
+    "elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts",
+    "elements/diff/gr-diff-view/gr-diff-view_html.ts",
+    "elements/diff/gr-diff/gr-diff_html.ts",
+    "elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts",
+    "elements/documentation/gr-documentation-search/gr-documentation-search_html.ts",
+    "elements/edit/gr-default-editor/gr-default-editor_html.ts",
+    "elements/edit/gr-edit-controls/gr-edit-controls_html.ts",
+    "elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.ts",
+    "elements/edit/gr-editor-view/gr-editor-view_html.ts",
+    "elements/gr-app-element_html.ts",
+    "elements/settings/gr-cla-view/gr-cla-view_html.ts",
+    "elements/settings/gr-edit-preferences/gr-edit-preferences_html.ts",
+    "elements/settings/gr-email-editor/gr-email-editor_html.ts",
+    "elements/settings/gr-identities/gr-identities_html.ts",
+    "elements/settings/gr-registration-dialog/gr-registration-dialog_html.ts",
+    "elements/settings/gr-settings-view/gr-settings-view_html.ts",
+    "elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
+    "elements/shared/gr-account-entry/gr-account-entry_html.ts",
+    "elements/shared/gr-account-label/gr-account-label_html.ts",
+    "elements/shared/gr-account-list/gr-account-list_html.ts",
+    "elements/shared/gr-autocomplete/gr-autocomplete_html.ts",
+    "elements/shared/gr-change-star/gr-change-star_html.ts",
+    "elements/shared/gr-change-status/gr-change-status_html.ts",
+    "elements/shared/gr-comment-thread/gr-comment-thread_html.ts",
+    "elements/shared/gr-comment/gr-comment_html.ts",
+    "elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts",
+    "elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.ts",
+    "elements/shared/gr-dialog/gr-dialog_html.ts",
+    "elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts",
+    "elements/shared/gr-download-commands/gr-download-commands_html.ts",
+    "elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts",
+    "elements/shared/gr-dropdown/gr-dropdown_html.ts",
+    "elements/shared/gr-editable-content/gr-editable-content_html.ts",
+    "elements/shared/gr-editable-label/gr-editable-label_html.ts",
+    "elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts",
+    "elements/shared/gr-label-info/gr-label-info_html.ts",
+    "elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts",
+    "elements/shared/gr-limited-text/gr-limited-text_html.ts",
+    "elements/shared/gr-linked-chip/gr-linked-chip_html.ts",
+    "elements/shared/gr-list-view/gr-list-view_html.ts",
+    "elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.ts",
+    "elements/shared/gr-textarea/gr-textarea_html.ts",
+]
+
+# Transform templates into a .ts files.
+templates_srcs = transform_polymer_templates(
+    name = "template_test",
+    srcs = glob(
+        [src_dir + "/**/*" + ext for src_dir in src_dirs for ext in [
+            ".ts",
+        ]],
+        exclude = [
+            "**/*_test.ts",
+        ] + ignore_templates_list,
+    ),
+    out_tsconfig = "tsconfig_template_test.json",
+    tsconfig = "tsconfig_bazel.json",
+    deps = [
+        "tsconfig.json",
+        "tsconfig_bazel.json",
+        "@ui_npm//:node_modules",
+    ],
+)
+
+# Compile transformed templates together with the polygerrit source. If
+# templates don't have problem, then the compilation ends without error.
+# Otherwise, the typescript compiler reports the error.
+# Note, that the compile_ts macro creates build rules. If the build succeed,
+# the macro creates the file compile_template_test.success. The
+# 'validate_polymer_templates' rule tests existence of the file.
+compile_ts(
+    name = "compile_template_test",
+    srcs = templates_srcs + glob(
+        [src_dir + "/**/*" + ext for src_dir in src_dirs for ext in [
+            ".ts",
+        ]],
+        exclude = [
+            "**/*_test.ts",
+        ] + ignore_templates_list,
+    ),
+    additional_deps = [
+        "tsconfig_bazel.json",
+    ],
+    emitJS = False,
+    # Should not run sandboxed.
+    tags = [
+        "local",
+        "manual",
+    ],
+    ts_outdir = "_pg_template_test_out",
+    ts_project = "tsconfig_template_test.json",
+)
+
+# This rule allows to run polymer template checker with bazel test command.
+# For details - see compile_template_test rule.
+sh_test(
+    name = "validate_polymer_templates",
+    srcs = [":empty_test.sh"],
+    data = ["compile_template_test.success"],
 )
 
 polygerrit_bundle(
diff --git a/polygerrit-ui/app/empty_test.sh b/polygerrit-ui/app/empty_test.sh
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/polygerrit-ui/app/empty_test.sh
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index feb1a82..5397655 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -34,49 +34,55 @@
         result.append(_get_ts_compiled_path(outdir, f))
     return result
 
-def compile_ts(name, srcs, ts_outdir, include_tests = False):
-    """Compiles srcs files with the typescript compiler
+def compile_ts(name, srcs, ts_outdir, additional_deps = [], ts_project = "tsconfig_bazel.json", emitJS = True, tags = []):
+    """Compiles srcs files with the typescript compiler. The following
+    dependencies are always passed:
+      the file specified by the ts_project argument
+      :tsconfig.json"
+      @ui_npm//:node_modules,
+    If compilation succeed, the file name+".success" is created. This is useful
+    for wrapping compilation in bazel test rules.
 
     Args:
       name: rule name
       srcs: list of input files (.js, .d.ts and .ts)
-      ts_outdir: typescript output directory
+      ts_outdir: typescript output directory; ignored if emitJS is True
+      additional_deps: list of additional dependencies for compilation
+      ts_project: the file with typescript project. If it extends another
+        typescript file, ensure that this other file is either in the default or
+        in the additional_deps dependencies.
+      emitJS: True - the rule generates JS output; otherwise(False) the rule
+        just run a compiler (for error checking)
 
     Returns:
-      The list of compiled files
+      The list of compiled JS files if emitJS is True; otherwise returns an
+      empty list
     """
     ts_rule_name = name + "_ts_compiled"
 
     # List of files produced by the typescript compiler
-    generated_js = _get_ts_output_files(ts_outdir, srcs)
+    generated_js = _get_ts_output_files(ts_outdir, srcs) if emitJS else []
 
     all_srcs = srcs + [
         ":tsconfig.json",
-        ":tsconfig_bazel.json",
         "@ui_npm//:node_modules",
-    ]
-    ts_project = "tsconfig_bazel.json"
+    ] + [ts_project] + additional_deps
 
-    if include_tests:
-        all_srcs = all_srcs + [
-            ":tsconfig_bazel_test.json",
-            "@ui_dev_npm//:node_modules",
-        ]
-        ts_project = "tsconfig_bazel_test.json"
+    success_out = name + ".success"
 
     # Run the compiler
     native.genrule(
         name = ts_rule_name,
         srcs = all_srcs,
-        outs = generated_js,
+        outs = generated_js + [success_out],
         cmd = " && ".join([
-            "$(location //tools/node_tools:tsc-bin) --project $(location :" +
-            ts_project +
-            ") --outdir $(RULEDIR)/" +
-            ts_outdir +
+            "$(location //tools/node_tools:tsc-bin) --project $(location :{})".format(ts_project) +
+            (" --outdir $(RULEDIR)/{}".format(ts_outdir) if emitJS else "") +
             " --baseUrl ./external/ui_npm/node_modules/",
+            "touch $(location {})".format(success_out),
         ]),
         tools = ["//tools/node_tools:tsc-bin"],
+        tags = tags,
     )
 
     return generated_js
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index 46d0173d..a533a0f 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -61,6 +61,7 @@
     "styles/**/*",
     "types/**/*",
     "utils/**/*",
-    "test/**/*"
+    "test/**/*",
+    "tmpl_out/**/*" //Created by template checker in dev-mode
   ]
 }
diff --git a/tools/js/template_checker.bzl b/tools/js/template_checker.bzl
new file mode 100644
index 0000000..da77234
--- /dev/null
+++ b/tools/js/template_checker.bzl
@@ -0,0 +1,136 @@
+# 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.
+# 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.
+
+"""This file contains macro to run polymer templates check."""
+
+load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary", "nodejs_test", "npm_package_bin", "params_file")
+load("@rules_pkg//:pkg.bzl", "pkg_tar")
+
+def _get_generated_files(outdir, srcs):
+    result = []
+    for f in srcs:
+        result.append(outdir + "/" + f)
+    return result
+
+def _generate_transformed_templates(name, srcs, tsconfig, deps, out_tsconfig, outdir, dev_run):
+    """Generates typescript code from polymer templates. It uses twinkie package
+    for generation.
+
+    Args:
+      name: rule name
+      srcs: all files in a project project
+      tsconfig: the original typescript project file
+      deps: dependencies
+      out_tsconfig: where to store the generated TS project.
+      outdir: where to store generated .ts files
+      dev_run: if True, the generator uses different file paths in generated
+        import statements. Later, generated files can be copied into workspace
+        for future debugging\\investigation templates issues.
+
+    Returns:
+      The list of generated files
+    """
+    generated_files = _get_generated_files(outdir, srcs)
+
+    # There is a limitation on the command-line length. Put all source files
+    # into a .params file (this is a text file, where each argument is placed
+    # on a new line)
+    params_file(
+        name = name + "_params",
+        out = name + ".params",
+        args = ["$(execpath {})".format(src) for src in srcs],
+        data = srcs,
+    )
+
+    # Arguments for twinkie
+    args = [
+        "$(location //tools/node_tools:twinkie-bin)",
+        "--tsconfig $(location {})".format(tsconfig),
+        "--out-dir $(RULEDIR)/{} ".format(outdir),
+        "--files $(location {})".format(name + ".params"),
+    ]
+    if dev_run:
+        args.append("--dev-run")
+    if out_tsconfig:
+        args.append("--out-ts-config $(location {})".format(out_tsconfig))
+
+    # Execute twinkie.
+    native.genrule(
+        name = name + "_npm_bin",
+        srcs = srcs + deps + [name + ".params"],
+        outs = generated_files + ([out_tsconfig] if out_tsconfig else []),
+        cmd = " ".join(args),
+        tools = ["//tools/node_tools:twinkie-bin"],
+        # Should not run sandboxed.
+        tags = [
+            "local",
+            "manual",
+        ],
+    )
+    return generated_files
+
+def transform_polymer_templates(name, srcs, tsconfig, deps, out_tsconfig):
+    """Transforms polymer templates into typescript code.
+    Additionally, the macro defines name+"_tar" package that contains
+    generated code with slightly different import paths.
+    Note, that polygerrit template tests don't depend on the tar package, so
+    bazel doesn't generate the tar package with the bazel test command.
+    The tar package must be build explicitly with the bazel build command.
+
+    Args:
+      name: rule name
+      srcs: all files in a project project
+      tsconfig: the original typescript project file
+      deps: dependencies
+      out_tsconfig: where to store the generated TS project.
+
+    Returns:
+      list of generated files
+    """
+
+    # Transformed templates for tests
+    generated_files = _generate_transformed_templates(
+        name = name,
+        srcs = srcs,
+        tsconfig = tsconfig,
+        deps = deps,
+        out_tsconfig = out_tsconfig,
+        dev_run = False,
+        outdir = name + "_out",
+    )
+
+    # Transformed templates for developers. Only the tar package depends
+    # on it and it never runs during tests.
+    generated_dev_files = _generate_transformed_templates(
+        name = name + "_dev",
+        srcs = srcs,
+        tsconfig = tsconfig,
+        deps = deps,
+        dev_run = True,
+        outdir = name + "_dev_out",
+        out_tsconfig = None,
+    )
+
+    # Pack all transformed files. Later files can be materialized in the
+    # WORKSPACE/polygerrit-ui/app/tmpl_out dir. The following command do it
+    # automatically
+    # npm run polytest:dev
+    pkg_tar(
+        name = name + "_tar",
+        srcs = generated_dev_files,
+        # Set strip_prefix to keep directory hierarchy in the .tar
+        # https://github.com/bazelbuild/rules_pkg/issues/82
+        strip_prefix = name + "_dev_out",
+    )
+    return generated_files
diff --git a/tools/node_tools/BUILD b/tools/node_tools/BUILD
index 03e3a13..0f836a0 100644
--- a/tools/node_tools/BUILD
+++ b/tools/node_tools/BUILD
@@ -45,3 +45,16 @@
     data = ["@tools_npm//:node_modules"],
     entry_point = "@tools_npm//:node_modules/typescript/lib/tsc.js",
 )
+
+# Wrap twinkie into a twinkie-bin binary.
+nodejs_binary(
+    name = "twinkie-bin",
+    # Point bazel to your node_modules to find the entry point
+    data = ["@npm//:node_modules"],
+    entry_point = "@npm//:node_modules/twinkie/src/app/index.js",
+    # Should not run sandboxed.
+    tags = [
+        "local",
+        "manual",
+    ],
+)
diff --git a/twinkie.patch b/twinkie.patch
new file mode 100644
index 0000000..0a61243
--- /dev/null
+++ b/twinkie.patch
@@ -0,0 +1,11 @@
+--- a/node_modules/twinkie/src/app/index.js
++++ b/node_modules/twinkie/src/app/index.js
+@@ -250,7 +250,7 @@ twinkie --tsconfig tsconfig.json --outdir output_dir [--files file_list] [--outt
+                 incremental: false,
+                 noEmit: true,
+             },
+-            files: [...allProgramFilesNames, generatedFiles],
++            files: [...allProgramFilesNames, ...generatedFiles],
+         };
+         fs.writeFileSync(cmdLineOptions.outputTsConfig, JSON.stringify(tsconfigContent, null, 2));
+     }
diff --git a/yarn.lock b/yarn.lock
index 45b4f2c..faa1e99 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1082,6 +1082,11 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
   integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
 
+"@types/minimatch@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
+  integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+
 "@types/minimist@^1.2.0":
   version "1.2.1"
   resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz"
@@ -2356,6 +2361,11 @@
     raw-body "2.4.0"
     type-is "~1.6.17"
 
+boolbase@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
 bower-config@^1.4.0, bower-config@^1.4.1:
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.3.tgz#3454fecdc5f08e7aa9cc6d556e492be0669689ae"
@@ -2432,7 +2442,7 @@
     widest-line "^3.1.0"
     wrap-ansi "^7.0.0"
 
-brace-expansion@^1.1.7:
+brace-expansion@^1.0.0, brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
   integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
@@ -2742,6 +2752,18 @@
   resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
   integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
 
+cheerio@1.0.0-rc.2:
+  version "1.0.0-rc.2"
+  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
+  integrity sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=
+  dependencies:
+    css-select "~1.2.0"
+    dom-serializer "~0.1.0"
+    entities "~1.1.1"
+    htmlparser2 "^3.9.1"
+    lodash "^4.15.0"
+    parse5 "^3.0.1"
+
 chokidar@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@@ -3253,6 +3275,16 @@
   resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz"
   integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
 
+css-select@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+  integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+  dependencies:
+    boolbase "~1.0.0"
+    css-what "2.1"
+    domutils "1.5.1"
+    nth-check "~1.0.1"
+
 css-slam@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/css-slam/-/css-slam-2.1.2.tgz#3d35b1922cb3e0002a45c89ab189492508c493e5"
@@ -3264,7 +3296,7 @@
     parse5 "^4.0.0"
     shady-css-parser "^0.1.0"
 
-css-what@^2.1.0:
+css-what@2.1, css-what@^2.1.0:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
   integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
@@ -3547,6 +3579,14 @@
   dependencies:
     esutils "^2.0.2"
 
+dom-serializer@0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+  dependencies:
+    domelementtype "^2.0.1"
+    entities "^2.0.0"
+
 dom-serializer@^1.0.1:
   version "1.3.1"
   resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz"
@@ -3556,6 +3596,14 @@
     domhandler "^4.0.0"
     entities "^2.0.0"
 
+dom-serializer@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+  integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+  dependencies:
+    domelementtype "^1.3.0"
+    entities "^1.1.1"
+
 dom-urls@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/dom-urls/-/dom-urls-1.1.0.tgz#001ddf81628cd1e706125c7176f53ccec55d918e"
@@ -3572,11 +3620,23 @@
     clone "^2.1.0"
     parse5 "^4.0.0"
 
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
 domelementtype@^2.0.1, domelementtype@^2.2.0:
   version "2.2.0"
   resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz"
   integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
 
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+  dependencies:
+    domelementtype "1"
+
 domhandler@^4.0.0, domhandler@^4.1.0:
   version "4.1.0"
   resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz"
@@ -3584,6 +3644,22 @@
   dependencies:
     domelementtype "^2.2.0"
 
+domutils@1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+  integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+domutils@^1.5.1:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
 domutils@^2.5.2:
   version "2.5.2"
   resolved "https://registry.npmjs.org/domutils/-/domutils-2.5.2.tgz"
@@ -3767,6 +3843,11 @@
   dependencies:
     ansi-colors "^4.1.1"
 
+entities@^1.1.1, entities@~1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
 entities@^2.0.0:
   version "2.2.0"
   resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz"
@@ -5355,6 +5436,18 @@
     relateurl "0.2.x"
     uglify-js "3.4.x"
 
+htmlparser2@^3.9.1:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+  dependencies:
+    domelementtype "^1.3.1"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^3.1.1"
+
 htmlparser2@^6.0.1:
   version "6.1.0"
   resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz"
@@ -6498,7 +6591,7 @@
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
   integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
 
-lodash@^4.0.0, lodash@^4.11.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.3.0:
+lodash@^4.0.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.3.0:
   version "4.17.21"
   resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -6901,6 +6994,13 @@
   dependencies:
     brace-expansion "^1.1.7"
 
+minimatch@3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
+  integrity sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=
+  dependencies:
+    brace-expansion "^1.0.0"
+
 minimist-options@4.1.0:
   version "4.1.0"
   resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz"
@@ -7172,6 +7272,13 @@
   dependencies:
     path-key "^3.0.0"
 
+nth-check@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+  dependencies:
+    boolbase "~1.0.0"
+
 number-is-nan@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -7527,6 +7634,13 @@
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
   integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
 
+parse5@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+  integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+  dependencies:
+    "@types/node" "*"
+
 parse5@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
@@ -9882,6 +9996,16 @@
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
+twinkie@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/twinkie/-/twinkie-1.1.2.tgz#c301e4fc26d00d61d3d7e5be030dc6a2264271da"
+  integrity sha512-4KwhyrcrRb0WWJKMX/aT+npmMZC0h+sA//+bLhNupmuKvesrH2vEZDe6yIr48FMWKEsdA2xNdQqw/3MapZ5qXQ==
+  dependencies:
+    "@types/minimatch" "3.0.3"
+    cheerio "1.0.0-rc.2"
+    minimatch "3.0.3"
+    typescript "4.0.5"
+
 type-check@^0.4.0, type-check@~0.4.0:
   version "0.4.0"
   resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
@@ -9939,6 +10063,11 @@
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
+typescript@4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
+  integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
+
 typescript@4.1.4:
   version "4.1.4"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.4.tgz#f058636e2f4f83f94ddaae07b20fd5e14598432f"