text: # Copyright 2017 The Bazel Authors. All rights reserved.
  #
  # 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.
  
  "js_library can be used to expose and share any library package."
  
  load(
      "//:providers.bzl",
      "DeclarationInfo",
      "ExternalNpmPackageInfo",
      "JSEcmaScriptModuleInfo",
      "JSModuleInfo",
      "JSNamedModuleInfo",
      "LinkablePackageInfo",
      "declaration_info",
      "js_ecma_script_module_info",
      "js_module_info",
      "js_named_module_info",
  )
  load(
      "//third_party/github.com/bazelbuild/bazel-skylib:rules/private/copy_file_private.bzl",
      "copy_bash",
      "copy_cmd",
  )
  
  _ATTRS = {
      "amd_names": attr.string_dict(
          doc = """Non-public legacy API, not recommended to make new usages.
          See documentation on AmdNamesInfo""",
      ),
      "deps": attr.label_list(),
      "is_windows": attr.bool(
          doc = "Internal use only. Automatically set by macro",
          mandatory = True,
      ),
      # module_name for legacy ts_library module_mapping support
      # which is still being used in a couple of tests
      # TODO: remove once legacy module_mapping is removed
      "module_name": attr.string(
          doc = "Internal use only. It will be removed soon.",
      ),
      "named_module_srcs": attr.label_list(
          doc = """Non-public legacy API, not recommended to make new usages.
          A subset of srcs that are javascript named-UMD or
          named-AMD for use in rules such as concatjs_devserver.
          They will be copied into the package bin folder if needed.""",
          allow_files = True,
      ),
      "package_name": attr.string(
          doc = """The package name that the linker will link this js_library as.
  
  If package_path is set, the linker will link this package under <package_path>/node_modules/<package_name>.
  If package_path is not set the this will be the root node_modules of the workspace.""",
      ),
      "package_path": attr.string(
          doc = """The package path in the workspace that the linker will link this js_library to.
  
  If package_path is set, the linker will link this package under <package_path>/node_modules/<package_name>.
  If package_path is not set the this will be the root node_modules of the workspace.""",
      ),
      "srcs": attr.label_list(allow_files = True),
      "strip_prefix": attr.string(
          doc = "Path components to strip from the start of the package import path",
          default = "",
      ),
  }
  
  AmdNamesInfo = provider(
      doc = "Non-public API. Provides access to the amd_names attribute of js_library",
      fields = {"names": """Mapping from require module names to global variables.
          This allows devmode JS sources to load unnamed UMD bundles from third-party libraries."""},
  )
  
  def write_amd_names_shim(actions, amd_names_shim, targets):
      """Shim AMD names for UMD bundles that were shipped anonymous.
  
      These are collected from our bootstrap deps (the only place global scripts should appear)
  
      Args:
        actions: starlark rule execution context.actions
        amd_names_shim: File where the shim is written
        targets: dependencies to be scanned for AmdNamesInfo providers
      """
  
      amd_names_shim_content = """// GENERATED by js_library.bzl
  // Shim these global symbols which were defined by a bootstrap script
  // so that they can be loaded with named require statements.
  """
      for t in targets:
          if AmdNamesInfo in t:
              for n in t[AmdNamesInfo].names.items():
                  amd_names_shim_content += "define(\"%s\", function() { return %s });\n" % n
      actions.write(amd_names_shim, amd_names_shim_content)
  
  JS_EXTENSIONS = ["js", "mjs", "cjs"]
  
  def _is_javascript_file(file):
      for extension in JS_EXTENSIONS:
          if file.basename.endswith(".%s" % extension) or \
             file.basename.endswith(".%s.map" % extension):
              return True
      return False
  
  def _to_manifest_path(ctx, file):
      if file.short_path.startswith("../"):
          return file.short_path[3:]
      else:
          return ctx.workspace_name + "/" + file.short_path
  
  def _link_path(ctx, all_files):
      link_path = "/".join([p for p in [ctx.bin_dir.path, ctx.label.workspace_root, ctx.label.package] if p])
  
      # Strip a prefix from the package require path
      if ctx.attr.strip_prefix:
          link_path += "/" + ctx.attr.strip_prefix
  
          # Check that strip_prefix contains at least one src path
          check_prefix = "/".join([p for p in [
              ctx.label.workspace_name if ctx.label.workspace_name else ctx.workspace_name,
              ctx.label.package,
              ctx.attr.strip_prefix,
          ] if p])
          prefix_contains_src = False
          for file in all_files:
              if _to_manifest_path(ctx, file).startswith(check_prefix):
                  prefix_contains_src = True
                  break
          if not prefix_contains_src:
              fail("js_library %s strip_prefix path does not contain any of the provided sources" % ctx.label)
  
      return link_path
  
  def _impl(ctx):
      input_files = ctx.files.srcs + ctx.files.named_module_srcs
      all_files = []
      typings = []
      js_files = []
      named_module_files = []
  
      for idx, f in enumerate(input_files):
          file = f
  
          # copy files into bin if needed
          if file.is_source and not file.path.startswith("external/"):
              dst = ctx.actions.declare_file(file.basename, sibling = file)
              if ctx.attr.is_windows:
                  copy_cmd(ctx, file, dst)
              else:
                  copy_bash(ctx, file, dst)
  
              # re-assign file to the one now copied into the bin folder
              file = dst
  
          # register js files
          if _is_javascript_file(file) or file.basename.endswith(".json"):
              js_files.append(file)
  
          # register typings
          if file.is_directory:
              # assume a directory contains typings since we can't know that it doesn't
              typings.append(file)
          elif (
              (
                  file.path.endswith(".d.ts") or
                  file.path.endswith(".d.ts.map") or
                  # package.json may be required to resolve "typings" key
                  file.path.endswith("/package.json")
              ) and
              # exclude eg. external/npm/node_modules/protobufjs/node_modules/@types/node/index.d.ts
              # these would be duplicates of the typings provided directly in another dependency.
              # also exclude all /node_modules/typescript/lib/lib.*.d.ts files as these are determined by
              # the tsconfig "lib" attribute
              len(file.path.split("/node_modules/")) < 3 and file.path.find("/node_modules/typescript/lib/lib.") == -1
          ):
              typings.append(file)
  
          # ctx.files.named_module_srcs are merged after ctx.files.srcs
          if idx >= len(ctx.files.srcs):
              named_module_files.append(file)
  
          # every single file on bin should be added here
          all_files.append(file)
  
      files_depset = depset(all_files)
      js_files_depset = depset(js_files)
      named_module_files_depset = depset(named_module_files)
      typings_depset = depset(typings)
  
      files_depsets = [files_depset]
      npm_sources_depsets = [files_depset]
      direct_ecma_script_module_depsets = [files_depset]
      direct_sources_depsets = [files_depset]
      direct_named_module_sources_depsets = [named_module_files_depset]
      typings_depsets = [typings_depset]
      js_files_depsets = [js_files_depset]
  
      for dep in ctx.attr.deps:
          if ExternalNpmPackageInfo in dep:
              npm_sources_depsets.append(dep[ExternalNpmPackageInfo].sources)
          else:
              if JSEcmaScriptModuleInfo in dep:
                  direct_ecma_script_module_depsets.append(dep[JSEcmaScriptModuleInfo].direct_sources)
                  direct_sources_depsets.append(dep[JSEcmaScriptModuleInfo].direct_sources)
              if JSModuleInfo in dep:
                  js_files_depsets.append(dep[JSModuleInfo].direct_sources)
                  direct_sources_depsets.append(dep[JSModuleInfo].direct_sources)
              if JSNamedModuleInfo in dep:
                  direct_named_module_sources_depsets.append(dep[JSNamedModuleInfo].direct_sources)
                  direct_sources_depsets.append(dep[JSNamedModuleInfo].direct_sources)
              if DeclarationInfo in dep:
                  typings_depsets.append(dep[DeclarationInfo].declarations)
                  direct_sources_depsets.append(dep[DeclarationInfo].declarations)
              if DefaultInfo in dep:
                  files_depsets.append(dep[DefaultInfo].files)
  
      runfiles = ctx.runfiles(
          files = all_files,
          transitive_files = depset(
              transitive = files_depsets + typings_depsets,
          ),
      )
      deps_runfiles = [d[DefaultInfo].default_runfiles for d in ctx.attr.deps]
  
      # Perf optimization available in newer Bazel releases
      if "merge_all" in dir(runfiles):
          runfiles = runfiles.merge_all(deps_runfiles)
      else:
          for d in deps_runfiles:
              runfiles = runfiles.merge(d)
  
      providers = [
          DefaultInfo(
              files = depset(transitive = files_depsets),
              runfiles = runfiles,
          ),
          AmdNamesInfo(names = ctx.attr.amd_names),
          js_ecma_script_module_info(
              sources = depset(transitive = direct_ecma_script_module_depsets),
              deps = ctx.attr.deps,
          ),
          js_module_info(
              sources = depset(transitive = js_files_depsets),
              deps = ctx.attr.deps,
          ),
          js_named_module_info(
              sources = depset(transitive = direct_named_module_sources_depsets),
              deps = ctx.attr.deps,
          ),
      ]
  
      if ctx.attr.package_name == "$node_modules$" or ctx.attr.package_name == "$node_modules_dir$":
          # special case for external npm deps
          workspace_name = ctx.label.workspace_name if ctx.label.workspace_name else ctx.workspace_name
          providers.append(ExternalNpmPackageInfo(
              direct_sources = depset(transitive = direct_sources_depsets),
              sources = depset(transitive = npm_sources_depsets),
              workspace = workspace_name,
              path = ctx.attr.package_path,
              has_directories = (ctx.attr.package_name == "$node_modules_dir$"),
          ))
      else:
          providers.append(LinkablePackageInfo(
              package_name = ctx.attr.package_name,
              package_path = ctx.attr.package_path,
              path = _link_path(ctx, all_files),
              files = depset(transitive = direct_sources_depsets),
          ))
  
      if len(typings) or len(typings_depsets) > 1:
          # Don't provide DeclarationInfo if there are no typings to provide.
          # Improves error messaging downstream if DeclarationInfo is required.
          decls = depset(transitive = typings_depsets)
          providers.append(declaration_info(
              declarations = decls,
              deps = ctx.attr.deps,
          ))
          providers.append(OutputGroupInfo(types = decls))
      elif ctx.attr.package_name == "$node_modules_dir$":
          # If this is directory artifacts npm package then always provide declaration_info
          # since we can't scan through files
          decls = depset(transitive = files_depsets)
          providers.append(declaration_info(
              declarations = decls,
              deps = ctx.attr.deps,
          ))
          providers.append(OutputGroupInfo(types = decls))
  
      return providers
  
  _js_library = rule(
      implementation = _impl,
      attrs = _ATTRS,
  )
  
  def js_library(
          name,
          srcs = [],
          package_name = None,
          package_path = "",
          deps = [],
          **kwargs):
      """Groups JavaScript code so that it can be depended on like an npm package.
  
      `js_library` is intended to be used internally within Bazel, such as between two libraries in your monorepo.
      This rule doesn't perform any build steps ("actions") so it is similar to a `filegroup`.
      However it provides several Bazel "Providers" for interop with other rules.
  
      > Compare this to `pkg_npm` which just produces a directory output, and therefore can't expose individual
      > files to downstream targets and causes a cascading re-build of all transitive dependencies when any file
      > changes. Also `pkg_npm` is intended to publish your code for external usage outside of Bazel, like
      > by publishing to npm or artifactory, while `js_library` is for internal dependencies within your repo.
  
      `js_library` also copies any source files into the bazel-out folder.
      This is the same behavior as the `copy_to_bin` rule.
      By copying the complete package to the output tree, we ensure that the linker (our `npm link` equivalent)
      will make your source files available in the node_modules tree where resolvers expect them.
      It also means you can have relative imports between the files
      rather than being forced to use Bazel's "Runfiles" semantics where any program might need a helper library
      to resolve files between the logical union of the source tree and the output tree.
  
      ### Example
  
      A typical example usage of `js_library` is to expose some sources with a package name:
  
      ```python
      ts_project(
          name = "compile_ts",
          srcs = glob(["*.ts"]),
      )
  
      js_library(
          name = "my_pkg",
          # Code that depends on this target can import from "@myco/mypkg"
          package_name = "@myco/mypkg",
          # Consumers might need fields like "main" or "typings"
          srcs = ["package.json"],
          # The .js and .d.ts outputs from above will be part of the package
          deps = [":compile_ts"],
      )
      ```
  
      > To help work with "named AMD" modules as required by `concatjs_devserver` and other Google-style "concatjs" rules,
      > `js_library` has some undocumented advanced features you can find in the source code or in our examples.
      > These should not be considered a public API and aren't subject to our usual support and semver guarantees.
  
      ### Outputs
  
      Like all Bazel rules it produces a default output by providing [DefaultInfo].
      You'll get these outputs if you include this in the `srcs` of a typical rule like `filegroup`,
      and these will be the printed result when you `bazel build //some:js_library_target`.
      The default outputs are all of:
      - [DefaultInfo] produced by targets in `deps`
      - A copy of all sources (InputArtifacts from your source tree) in the bazel-out directory
  
      When there are TypeScript typings files, `js_library` provides [DeclarationInfo](#declarationinfo)
      so this target can be a dependency of a TypeScript rule. This includes any `.d.ts` files in `srcs` as well
      as transitive ones from `deps`.
      It will also provide [OutputGroupInfo] with a "types" field, so you can select the typings outputs with
      `bazel build //some:js_library_target --output_groups=types` or with a `filegroup` rule using the
      [output_group] attribute.
  
      In order to work with the linker (similar to `npm link` for first-party monorepo deps), `js_library` provides
      [LinkablePackageInfo](#linkablepackageinfo) for use with our "linker" that makes this package importable.
  
      It also provides:
      - [ExternalNpmPackageInfo](#externalnpmpackageinfo) to interop with rules that expect third-party npm packages.
      - [JSModuleInfo](#jsmoduleinfo) so rules like bundlers can collect the transitive set of .js files
      - [JSNamedModuleInfo](#jsnamedmoduleinfo) for rules that expect named AMD or `goog.module` format JS
  
      [OutputGroupInfo]: https://docs.bazel.build/versions/main/skylark/lib/OutputGroupInfo.html
      [DefaultInfo]: https://docs.bazel.build/versions/main/skylark/lib/DefaultInfo.html
      [output_group]: https://docs.bazel.build/versions/main/be/general.html#filegroup.output_group
  
      Args:
          name: The name for the target
          srcs: The list of files that comprise the package
          package_name: The name it will be imported by. Should match the "name" field in the package.json file.
  
              If package_name == "$node_modules$" this indictates that this js_library target is one or more external npm
              packages in node_modules. This is a special case that used be covered by the internal only
              `external_npm_package` attribute. NB: '$' is an illegal character
              for npm packages names so this reserved name will not conflict with any valid package_name values
  
              This is used by the yarn_install & npm_install repository rules for npm dependencies installed by
              yarn & npm. When true, js_library will provide ExternalNpmPackageInfo.
  
              It can also be used for user-managed npm dependencies if node_modules is layed out outside of bazel.
              For example,
  
              ```starlark
              js_library(
                  name = "node_modules",
                  srcs = glob(
                      include = [
                          "node_modules/**/*.js",
                          "node_modules/**/*.d.ts",
                          "node_modules/**/*.json",
                          "node_modules/.bin/*",
                      ],
                      exclude = [
                          # Files under test & docs may contain file names that
                          # are not legal Bazel labels (e.g.,
                          # node_modules/ecstatic/test/public/中文/檔案.html)
                          "node_modules/**/test/**",
                          "node_modules/**/docs/**",
                          # Files with spaces in the name are not legal Bazel labels
                          "node_modules/**/* */**",
                          "node_modules/**/* *",
                      ],
                  ),
                  # Special value to provide ExternalNpmPackageInfo which is used by downstream
                  # rules that use these npm dependencies
                  package_name = "$node_modules$",
              )
              ```
  
              See `examples/user_managed_deps` for a working example of user-managed npm dependencies.
          package_path: The directory in the workspace to link to.
              If set, link this js_library to the node_modules under the package path specified.
              If unset, the default is to link to the node_modules root of the workspace.
          deps: Other targets that provide JavaScript code
          **kwargs: Other attributes
      """
  
      # Undocumented features
      amd_names = kwargs.pop("amd_names", {})
      module_name = kwargs.pop("module_name", None)
      named_module_srcs = kwargs.pop("named_module_srcs", [])
  
      if module_name:
          fail("use package_name instead of module_name in target //%s:%s" % (native.package_name(), name))
      if kwargs.pop("is_windows", None):
          fail("is_windows is set by the js_library macro and should not be set explicitly")
  
      _js_library(
          name = name,
          amd_names = amd_names,
          srcs = srcs,
          named_module_srcs = named_module_srcs,
          deps = deps,
          package_name = package_name,
          package_path = package_path,
          # module_name for legacy ts_library module_mapping support
          # which is still being used in a couple of tests
          # TODO: remove once legacy module_mapping is removed
          module_name = package_name if package_name != "$node_modules$" and package_name != "$node_modules_dir$" else None,
          is_windows = select({
              "@bazel_tools//src/conditions:host_windows": True,
              "//conditions:default": False,
          }),
          **kwargs
      )
  