chromium/ui/webui/resources/tools/build_webui.gni

# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("//build/config/features.gni")
import(
    "//tools/code_coverage/js_source_maps/create_js_source_maps/create_js_source_maps.gni")
import(
    "//tools/code_coverage/js_source_maps/merge_js_source_maps/merge_js_source_maps.gni")
import("//tools/grit/grit_rule.gni")
import("//tools/grit/preprocess_if_expr.gni")
import("//tools/polymer/css_to_wrapper.gni")
import("//tools/polymer/html_to_wrapper.gni")
import("//tools/typescript/ts_library.gni")
import("//tools/typescript/webui_path_mappings.gni")
import("//ui/webui/resources/tools/bundle_js.gni")
import("//ui/webui/resources/tools/bundle_js_excludes.gni")
import("//ui/webui/resources/tools/generate_grd.gni")
import("//ui/webui/resources/tools/minify_js.gni")
import("//ui/webui/webui_features.gni")

if (use_blink) {
  import("//chrome/common/features.gni")
}

# See documentation at https://chromium.googlesource.com/chromium/src/+/HEAD/docs/webui_build_configuration.md#build_webui
template("build_webui") {
  not_needed([ "target_name" ])

  forward_variables_from(invoker, [ "grd_prefix" ])

  preprocess_dir = "${target_gen_dir}/preprocessed"

  ts_out_dir = "${target_gen_dir}/tsc"
  if (defined(invoker.ts_out_dir)) {
    ts_out_dir = invoker.ts_out_dir
  }

  optimize = optimize_webui
  if (defined(invoker.optimize)) {
    optimize = invoker.optimize
  }

  minify = optimize
  bundle = optimize && defined(invoker.optimize_webui_in_files)

  has_ts_in_files = defined(invoker.web_component_files) ||
                    defined(invoker.non_web_component_files)
  enable_source_maps = enable_webui_inline_sourcemaps && has_ts_in_files
  if (defined(invoker.enable_source_maps) && has_ts_in_files) {
    assert(!invoker.enable_source_maps || !optimize)
    enable_source_maps = invoker.enable_source_maps
  }

  # Determine whether a :build_grd or :build_grdp target should be defined.
  generate_grdp = false
  generate_grd_target_name = "build_grd"
  if (defined(invoker.generate_grdp) && invoker.generate_grdp) {
    generate_grdp = true
    generate_grd_target_name = "build_grdp"
  }

  ### Compute the lists of files that are used across multiple targets.

  # At least one of `web_component_files`, `css_files`, `mojo_files`, or
  # 'non_web_component_files` must be defined. Otherwise, the build rule
  # does not actually generate any input for TS compiler.
  assert(has_ts_in_files || defined(invoker.css_files) ||
         defined(invoker.mojo_files))

  ts_files = []
  if (defined(invoker.web_component_files)) {
    ts_files += invoker.web_component_files
  }
  if (defined(invoker.non_web_component_files)) {
    ts_files += invoker.non_web_component_files
  }

  if (defined(invoker.web_component_files) ||
      defined(invoker.icons_html_files)) {
    # Files that are passed as input to html_to_wrapper().
    html_files = []
    if (defined(invoker.web_component_files)) {
      web_component_ts_files =
          filter_include(invoker.web_component_files, [ "*.ts" ])
      foreach(f, web_component_ts_files) {
        html_files += [ string_replace(f, ".ts", ".html") ]
      }

      web_component_js_files =
          filter_include(invoker.web_component_files, [ "*.js" ])
      foreach(f, web_component_js_files) {
        html_files += [ string_replace(f, ".js", ".html") ]
      }
    }
    if (defined(invoker.icons_html_files)) {
      html_files += invoker.icons_html_files
    }

    # Files that are generated by html_to_wrapper().
    html_wrapper_files = []
    foreach(f, html_files) {
      html_wrapper_files += [ f + ".ts" ]
    }
  }

  if (defined(invoker.css_files)) {
    # Files that are generated by css_to_wrapper().
    css_wrapper_files = []
    foreach(f, invoker.css_files) {
      css_wrapper_files += [ f + ".ts" ]
    }
  }

  # Generated Mojo JS files.
  if (defined(invoker.mojo_files)) {
    assert(defined(invoker.mojo_files_deps))
    mojo_files = []
    foreach(mojo_file, invoker.mojo_files) {
      mojo_files += [ get_path_info(invoker.mojo_files, "file") ]
    }
  }

  if (defined(invoker.static_files)) {
    # Compute which static_files should be preprocessed.
    non_preprocessed_files_filter = [
      "*.jpg",
      "*.png",
      "*.svg",
    ]
    static_non_preprocessed_files =
        filter_include(invoker.static_files, non_preprocessed_files_filter)
    static_preprocessed_files =
        filter_exclude(invoker.static_files, non_preprocessed_files_filter)
  }

  preprocessor_defines = []
  if (use_blink) {
    preprocessor_defines += chrome_grit_defines
  }
  if (defined(invoker.preprocessor_defines)) {
    preprocessor_defines += invoker.preprocessor_defines
  }

  ### Define the various targets that are required by the build pipeline.

  # Specifically the order in which these targets are executed is:
  #
  #  1) preprocess_if_expr()
  #  2) create_js_source_maps() (only if |invoker.enable_source_maps| flag is
  #     true)
  #  3) html_to_wrapper(), css_to_wrapper()
  #  4) ts_library()
  #  5) merge_js_source_maps() (only if the |invoker.enable_source_maps| flag is
  #     true)
  #  6) optimize_webui() (only if invoker.optimize && defined(invoker.optimize_webui_in_files))
  #  7) minify_js() (only if invoker.optimize && !defined(invoker.optimize_webui_in_files))
  #  8) generate_grd()
  #  9) grit()

  if (defined(invoker.static_files)) {
    preprocess_if_expr("preprocess_static_files") {
      visibility = [ ":$generate_grd_target_name" ]

      defines = preprocessor_defines

      in_folder = "."
      out_folder = preprocess_dir
      in_files = static_preprocessed_files
      out_manifest = "${target_gen_dir}/preprocess_static_files_manifest.json"
    }
  }

  if (has_ts_in_files) {
    preprocess_if_expr("preprocess_ts_files") {
      visibility = [
        ":build_ts",
        ":html_wrapper_files",
      ]
      if (enable_source_maps) {
        visibility += [ ":create_source_maps" ]
      }

      defines = preprocessor_defines

      in_folder = "."
      out_folder = preprocess_dir
      if (enable_source_maps) {
        out_folder = "${preprocess_dir}/_tmp"
        enable_removal_comments = enable_source_maps
      }
      in_files = ts_files
    }
  }

  if (enable_source_maps) {
    create_js_source_maps("create_source_maps") {
      # TODO(dpapad): Make this only visible to ":build_ts" and
      # ":html_wrapper_files".
      inline_sourcemaps = true

      sources = filter_include(
              get_target_outputs(":preprocess_ts_files"),
              [
                "*.ts",

                # TODO(crbug.com/40167175): Remove once we no longer pass JS
                # files to build_webui.
                "*.js",
              ])
      originals = []
      outputs = []

      foreach(in_file, sources) {
        rebased_path = rebase_path(in_file, "${preprocess_dir}/_tmp")
        originals += [ rebased_path ]
        outputs += [ "${preprocess_dir}/" + rebased_path ]
      }
      deps = [ ":preprocess_ts_files" ]
    }
  }

  if (defined(html_files) || defined(invoker.css_files)) {
    preprocess_if_expr("preprocess_html_css_files") {
      visibility = [
        ":css_wrapper_files",
        ":html_wrapper_files",
      ]

      defines = preprocessor_defines

      in_folder = "."
      out_folder = preprocess_dir

      in_files = []
      if (defined(html_files)) {
        in_files += html_files
      }

      if (defined(invoker.css_files)) {
        in_files += invoker.css_files
      }
    }
  }

  if (defined(html_files)) {
    html_to_wrapper("html_wrapper_files") {
      visibility = [ ":build_ts" ]
      deps = [ ":preprocess_html_css_files" ]
      in_folder = preprocess_dir
      out_folder = preprocess_dir
      in_files = html_files
      minify = optimize

      if (defined(invoker.html_to_wrapper_scheme)) {
        scheme = invoker.html_to_wrapper_scheme
      }

      if (defined(invoker.html_to_wrapper_template)) {
        template = invoker.html_to_wrapper_template
        if (invoker.html_to_wrapper_template == "detect") {
          if (enable_source_maps) {
            deps += [ ":create_source_maps" ]
          } else {
            deps += [ ":preprocess_ts_files" ]
          }
        }
      }
    }
  }

  if (defined(invoker.css_files)) {
    css_to_wrapper("css_wrapper_files") {
      visibility = [ ":build_ts" ]
      deps = [ ":preprocess_html_css_files" ]
      in_folder = preprocess_dir
      out_folder = preprocess_dir
      in_files = invoker.css_files
      minify = optimize
    }
  }

  if (defined(invoker.mojo_files_deps)) {
    mojo_base_path = "."
    if (defined(invoker.mojo_base_path)) {
      mojo_base_path = invoker.mojo_base_path
    }

    copy("copy_mojo") {
      visibility = [ ":build_ts" ]
      deps = invoker.mojo_files_deps
      sources = invoker.mojo_files
      outputs = [ "${preprocess_dir}/${mojo_base_path}/{{source_file_part}}" ]
    }
  }

  if (defined(invoker.ts_deps)) {
    webui_path_mappings("build_path_map") {
      ts_deps = invoker.ts_deps
      visibility = [ ":build_ts" ]

      webui_context_type = "relative"
      if (defined(invoker.webui_context_type)) {
        webui_context_type = invoker.webui_context_type
      }
    }
  }

  ts_library("build_ts") {
    root_dir = preprocess_dir
    out_dir = ts_out_dir

    composite = false
    if (defined(invoker.ts_composite)) {
      composite = invoker.ts_composite
    }

    if (!composite) {
      visibility = [
        ":$generate_grd_target_name",
        ":build_bundle",
        ":build_min_js",
      ]

      if (enable_source_maps) {
        visibility += [ ":merge_source_maps" ]
      }
    }

    if (defined(invoker.ts_tsconfig_base)) {
      tsconfig_base = invoker.ts_tsconfig_base
    } else if (defined(invoker.ts_deps)) {
      # Default tsconfig is set to tsconfig_base_polymer.json for targets
      # with a Polymer dependency, set to tsconfig_base_lit.json for
      # targets with a Lit dependency and no Polymer dependency, and left
      # unset (defaults to //tools/typescript/tsconfig_base.json) for other
      # non-Polymer, non-Lit targets.
      has_polymer =
          filter_include(invoker.ts_deps,
                         [ "//third_party/polymer/v3_0:library" ]) != []
      if (has_polymer) {
        tsconfig_base = "//tools/typescript/tsconfig_base_polymer.json"
      } else {
        has_lit = filter_include(invoker.ts_deps,
                                 [ "//third_party/lit/v3_0:build_ts" ]) != []
        if (has_lit) {
          tsconfig_base = "//tools/typescript/tsconfig_base_lit.json"
        }
      }
    }

    in_files = ts_files
    if (defined(invoker.ts_deps)) {
      path_mappings_file = "$target_gen_dir/path_mappings_build_path_map.json"
      extra_deps = [ ":build_path_map" ]
    } else {
      extra_deps = []
    }
    if (enable_source_maps) {
      # ts_library()'s |enable_source_maps| param inherited from the outer
      # scope's enable_source_maps
      extra_deps += [ ":create_source_maps" ]
    } else if (has_ts_in_files) {
      extra_deps += [ ":preprocess_ts_files" ]
    }

    if (defined(html_files)) {
      in_files += html_wrapper_files
      extra_deps += [ ":html_wrapper_files" ]
    }

    if (defined(invoker.css_files)) {
      in_files += css_wrapper_files
      extra_deps += [ ":css_wrapper_files" ]
    }

    if (defined(invoker.ts_deps)) {
      deps = invoker.ts_deps
    }

    if (defined(invoker.ts_definitions)) {
      definitions = invoker.ts_definitions
    }

    if (defined(invoker.ts_path_mappings)) {
      path_mappings = invoker.ts_path_mappings
    }

    if (defined(invoker.mojo_files_deps)) {
      assert(defined(invoker.mojo_files))
      target_outputs = get_target_outputs(":copy_mojo")

      # Add all Mojo JS files produced by `:copy_mojo` as inputs to the TS
      # compiler.
      foreach(o, target_outputs) {
        in_files += [ rebase_path(o, preprocess_dir) ]
      }
      extra_deps += [ ":copy_mojo" ]
    }

    if (defined(invoker.ts_extra_deps)) {
      extra_deps += invoker.ts_extra_deps
    }
  }

  if (enable_source_maps) {
    merge_js_source_maps("merge_source_maps") {
      deps = [ ":build_ts" ]
      manifest_files =
          filter_include(get_target_outputs(":build_ts"), [ "*_manifest.json" ])
      js_files = filter_include(get_target_outputs(":build_ts"), [ "*.js" ])

      sources = []
      outputs = []

      out_dir = "$target_gen_dir/merge_source_maps"
      foreach(_js_file, js_files) {
        sources += [ _js_file ]
        outputs += [ string_replace(_js_file, ts_out_dir, out_dir) ]
      }
    }
  }

  if (bundle) {
    bundle_js("build_bundle") {
      visibility = [ ":build_min_js" ]
      host = invoker.optimize_webui_host
      input = rebase_path(ts_out_dir, root_build_dir)
      js_module_in_files = invoker.optimize_webui_in_files
      out_folder = "$target_gen_dir/bundled"
      excludes = BUNDLE_JS_EXCLUDES
      if (defined(invoker.optimize_webui_excludes)) {
        excludes += invoker.optimize_webui_excludes
      }

      if (defined(invoker.optimize_webui_external_paths)) {
        external_paths = invoker.optimize_webui_external_paths
      }

      deps = [ ":build_ts" ]
    }
  }

  if (minify) {
    minify_js("build_min_js") {
      visibility = [ ":$generate_grd_target_name" ]
      in_folder = ts_out_dir
      in_target = ":build_ts"
      if (bundle) {
        in_folder = "$target_gen_dir/bundled"
        in_target = ":build_bundle"
      }
      out_folder = "$target_gen_dir/minified"

      js_files = filter_include(get_target_outputs(in_target), [ "*.js" ])
      in_files = []
      foreach(_js_file, js_files) {
        in_files += [ string_replace(_js_file, "$in_folder/", "") ]
      }

      deps = [ in_target ]
    }
  }

  generate_grd(generate_grd_target_name) {
    if (!generate_grdp) {
      visibility = [ ":resources_grit" ]
    }

    grd_prefix = grd_prefix
    out_grd = "$target_gen_dir/resources.grd"
    if (generate_grdp) {
      out_grd = "$target_gen_dir/resources.grdp"
    }

    deps = []
    manifest_files = []

    if (defined(invoker.static_files)) {
      input_files = static_non_preprocessed_files
      input_files_base_dir = rebase_path(".", "//")

      deps += [ ":preprocess_static_files" ]
      manifest_files +=
          [ "${target_gen_dir}/preprocess_static_files_manifest.json" ]
    }

    if (optimize) {
      deps += [ ":build_min_js" ]
      manifest_files += filter_include(get_target_outputs(":build_min_js"),
                                       [ "*_manifest.json" ])
      if (bundle) {
        resource_path_rewrites = []
        foreach(f, invoker.optimize_webui_in_files) {
          name = get_path_info(f, "name")
          output_file = string_replace(f, "${name}.js", "${name}.rollup.js")
          resource_path_rewrites += [ "${output_file}|${f}" ]
        }
      }
    } else if (enable_source_maps) {
      deps += [ ":merge_source_maps" ]
      manifest_files += filter_include(get_target_outputs(":merge_source_maps"),
                                       [ "*_manifest__processed.json" ])
    } else {
      deps += [ ":build_ts" ]
      manifest_files +=
          filter_include(get_target_outputs(":build_ts"), [ "*_manifest.json" ])
    }

    if (defined(invoker.extra_grdp_deps)) {
      deps += invoker.extra_grdp_deps
      grdp_files = invoker.extra_grdp_files
    }

    if (defined(invoker.grd_resource_path_prefix)) {
      resource_path_prefix = invoker.grd_resource_path_prefix
    }
  }

  if (!generate_grdp) {
    grit("resources") {
      # These arguments are needed since the grd is generated at build time.
      enable_input_discovery_for_gn_analyze = false
      source = "$target_gen_dir/resources.grd"
      deps = [ ":$generate_grd_target_name" ]

      outputs = [
        "grit/${grd_prefix}_resources.h",
        "grit/${grd_prefix}_resources_map.cc",
        "grit/${grd_prefix}_resources_map.h",
        "${grd_prefix}_resources.pak",
      ]

      grit_output_dir = "$root_gen_dir/chrome"
      if (defined(invoker.grit_output_dir)) {
        grit_output_dir = invoker.grit_output_dir
      }

      output_dir = grit_output_dir
    }
  }
}