blob: 16395468c1d193443515a8ed2cc9008c0be1a0fd [file] [log] [blame]
# 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.
"TypeScript compilation"
load("@build_bazel_rules_nodejs//internal/common:node_module_info.bzl", "NodeModuleInfo", "collect_node_modules_aspect")
# pylint: disable=unused-argument
# pylint: disable=missing-docstring
load(":common/compilation.bzl", "COMMON_ATTRIBUTES", "DEPS_ASPECTS", "compile_ts", "ts_providers_dict_to_struct")
load(":common/tsconfig.bzl", "create_tsconfig")
load(":ts_config.bzl", "TsConfigInfo")
_DEFAULT_COMPILER = "@npm//@bazel/typescript/bin:tsc_wrapped"
def _trim_package_node_modules(package_name):
# trim a package name down to its path prior to a node_modules
# segment. 'foo/node_modules/bar' would become 'foo' and
# 'node_modules/bar' would become ''
segments = []
for n in package_name.split("/"):
if n == "node_modules":
break
segments += [n]
return "/".join(segments)
def _compute_node_modules_root(ctx):
"""Computes the node_modules root from the node_modules and deps attributes.
Args:
ctx: the skylark execution context
Returns:
The node_modules root as a string
"""
node_modules_root = None
if ctx.files.node_modules:
# ctx.files.node_modules is not an empty list
node_modules_root = "/".join([f for f in [
ctx.attr.node_modules.label.workspace_root,
_trim_package_node_modules(ctx.attr.node_modules.label.package),
"node_modules",
] if f])
for d in ctx.attr.deps:
if NodeModuleInfo in d:
possible_root = "/".join(["external", d[NodeModuleInfo].workspace, "node_modules"])
if not node_modules_root:
node_modules_root = possible_root
elif node_modules_root != possible_root:
fail("All npm dependencies need to come from a single workspace. Found '%s' and '%s'." % (node_modules_root, possible_root))
if not node_modules_root:
# there are no fine grained deps and the node_modules attribute is an empty filegroup
# but we still need a node_modules_root even if its empty
node_modules_root = "/".join([f for f in [
ctx.attr.node_modules.label.workspace_root,
ctx.attr.node_modules.label.package,
"node_modules",
] if f])
return node_modules_root
def _filter_ts_inputs(all_inputs):
return [
f
for f in all_inputs
if f.path.endswith(".js") or f.path.endswith(".ts") or f.path.endswith(".json")
]
def _compile_action(ctx, inputs, outputs, tsconfig_file, node_opts, description = "prodmode"):
externs_files = []
action_inputs = []
action_outputs = []
for output in outputs:
if output.basename.endswith(".externs.js"):
externs_files.append(output)
elif output.basename.endswith(".es5.MF"):
ctx.actions.write(output, content = "")
else:
action_outputs.append(output)
# TODO(plf): For now we mock creation of files other than {name}.js.
for externs_file in externs_files:
ctx.actions.write(output = externs_file, content = "")
# A ts_library that has only .d.ts inputs will have no outputs,
# therefore there are no actions to execute
if not action_outputs:
return None
action_inputs.extend(_filter_ts_inputs(ctx.files.node_modules))
# Also include files from npm fine grained deps as action_inputs.
# These deps are identified by the NodeModuleInfo provider.
for d in ctx.attr.deps:
if NodeModuleInfo in d:
action_inputs.extend(_filter_ts_inputs(d.files.to_list()))
if ctx.file.tsconfig:
action_inputs.append(ctx.file.tsconfig)
if TsConfigInfo in ctx.attr.tsconfig:
action_inputs.extend(ctx.attr.tsconfig[TsConfigInfo].deps)
# Pass actual options for the node binary in the special "--node_options" argument.
arguments = ["--node_options=%s" % opt for opt in node_opts]
# One at-sign makes this a params-file, enabling the worker strategy.
# Two at-signs escapes the argument so it's passed through to tsc_wrapped
# rather than the contents getting expanded.
if ctx.attr.supports_workers:
arguments.append("@@" + tsconfig_file.path)
mnemonic = "TypeScriptCompile"
else:
arguments.append("-p")
arguments.append(tsconfig_file.path)
mnemonic = "tsc"
ctx.actions.run(
progress_message = "Compiling TypeScript (%s) %s" % (description, ctx.label),
mnemonic = mnemonic,
inputs = depset(action_inputs, transitive = [inputs]),
outputs = action_outputs,
# Use the built-in shell environment
# Allow for users who set a custom shell that can locate standard binaries like tr and uname
# See https://github.com/NixOS/nixpkgs/issues/43955#issuecomment-407546331
use_default_shell_env = True,
arguments = arguments,
executable = ctx.executable.compiler,
execution_requirements = {
"supports-workers": str(int(ctx.attr.supports_workers)),
},
)
# Enable the replay_params in case an aspect needs to re-build this library.
return struct(
label = ctx.label,
tsconfig = tsconfig_file,
inputs = depset(action_inputs, transitive = [inputs]),
outputs = action_outputs,
compiler = ctx.executable.compiler,
)
def _devmode_compile_action(ctx, inputs, outputs, tsconfig_file, node_opts):
_compile_action(
ctx,
inputs,
outputs,
tsconfig_file,
node_opts,
description = "devmode",
)
def tsc_wrapped_tsconfig(
ctx,
files,
srcs,
devmode_manifest = None,
jsx_factory = None,
**kwargs):
"""Produce a tsconfig.json that sets options required under Bazel.
"""
# The location of tsconfig.json is interpreted as the root of the project
# when it is passed to the TS compiler with the `-p` option:
# https://www.typescriptlang.org/docs/handbook/tsconfig-json.html.
# Our tsconfig.json is in bazel-foo/bazel-out/local-fastbuild/bin/{package_path}
# because it's generated in the execution phase. However, our source files are in
# bazel-foo/ and therefore we need to strip some parent directories for each
# f.path.
node_modules_root = _compute_node_modules_root(ctx)
config = create_tsconfig(
ctx,
files,
srcs,
devmode_manifest = devmode_manifest,
node_modules_root = node_modules_root,
**kwargs
)
config["bazelOptions"]["nodeModulesPrefix"] = node_modules_root
# Override the target so we use es2015 for devmode
# Since g3 isn't ready to do this yet
config["compilerOptions"]["target"] = "es2015"
# If the user gives a tsconfig attribute, the generated file should extend
# from the user's tsconfig.
# See https://github.com/Microsoft/TypeScript/issues/9876
# We subtract the ".json" from the end before handing to TypeScript because
# this gives extra error-checking.
if ctx.file.tsconfig:
workspace_path = config["compilerOptions"]["rootDir"]
config["extends"] = "/".join([workspace_path, ctx.file.tsconfig.path[:-len(".json")]])
if jsx_factory:
config["compilerOptions"]["jsxFactory"] = jsx_factory
tsetse_disabled_rules = []
# Matches section in javascript/typescript/tsconfig.bzl
# TODO(alexeagle): make them share code
if ctx.label.workspace_root.startswith("external/"):
# Violated by rxjs
tsetse_disabled_rules += ["ban-promise-as-condition"]
# For local testing
tsetse_disabled_rules += ["check-return-value"]
config["compilerOptions"]["plugins"] = [{
"name": "@bazel/tsetse",
"disabledRules": tsetse_disabled_rules,
}]
return config
# ************ #
# ts_library #
# ************ #
def _ts_library_impl(ctx):
"""Implementation of ts_library.
Args:
ctx: the context.
Returns:
the struct returned by the call to compile_ts.
"""
ts_providers = compile_ts(
ctx,
is_library = True,
# Filter out the node_modules from deps passed to TypeScript compiler
# since they don't have the required providers.
# They were added to the action inputs for tsc_wrapped already.
# strict_deps checking currently skips node_modules.
# TODO(alexeagle): turn on strict deps checking when we have a real
# provider for JS/DTS inputs to ts_library.
deps = [d for d in ctx.attr.deps if not NodeModuleInfo in d],
compile_action = _compile_action,
devmode_compile_action = _devmode_compile_action,
tsc_wrapped_tsconfig = tsc_wrapped_tsconfig,
)
return ts_providers_dict_to_struct(ts_providers)
local_deps_aspects = [collect_node_modules_aspect]
# Workaround skydoc bug which assumes DEPS_ASPECTS is a str type
[local_deps_aspects.append(a) for a in DEPS_ASPECTS]
ts_library = rule(
_ts_library_impl,
attrs = dict(COMMON_ATTRIBUTES, **{
"srcs": attr.label_list(
doc = "The TypeScript source files to compile.",
allow_files = [".ts", ".tsx"],
mandatory = True,
),
"compile_angular_templates": attr.bool(
doc = """Run the Angular ngtsc compiler under ts_library""",
),
"compiler": attr.label(
doc = """Sets a different TypeScript compiler binary to use for this library.
For example, we use the vanilla TypeScript tsc.js for bootstrapping,
and Angular compilations can replace this with `ngc`.
The default ts_library compiler depends on the `@npm//@bazel/typescript`
target which is setup for projects that use bazel managed npm deps that
fetch the @bazel/typescript npm package. It is recommended that you use
the workspace name `@npm` for bazel managed deps so the default
compiler works out of the box. Otherwise, you'll have to override
the compiler attribute manually.
""",
default = Label(_DEFAULT_COMPILER),
allow_files = True,
executable = True,
cfg = "host",
),
"internal_testing_type_check_dependencies": attr.bool(default = False, doc = "Testing only, whether to type check inputs that aren't srcs."),
"node_modules": attr.label(
doc = """The npm packages which should be available during the compile.
The default value is `@npm//typescript:typescript__typings` is setup
for projects that use bazel managed npm deps that. It is recommended
that you use the workspace name `@npm` for bazel managed deps so the
default node_modules works out of the box. Otherwise, you'll have to
override the node_modules attribute manually. This default is in place
since ts_library will always depend on at least the typescript
default libs which are provided by `@npm//typescript:typescript__typings`.
This attribute is DEPRECATED. As of version 0.18.0 the recommended
approach to npm dependencies is to use fine grained npm dependencies
which are setup with the `yarn_install` or `npm_install` rules.
For example, in targets that used a `//:node_modules` filegroup,
```
ts_library(
name = "my_lib",
...
node_modules = "//:node_modules",
)
```
which specifies all files within the `//:node_modules` filegroup
to be inputs to the `my_lib`. Using fine grained npm dependencies,
`my_lib` is defined with only the npm dependencies that are
needed:
```
ts_library(
name = "my_lib",
...
deps = [
"@npm//@types/foo",
"@npm//@types/bar",
"@npm//foo",
"@npm//bar",
...
],
)
```
In this case, only the listed npm packages and their
transitive deps are includes as inputs to the `my_lib` target
which reduces the time required to setup the runfiles for this
target (see https://github.com/bazelbuild/bazel/issues/5153).
The default typescript libs are also available via the node_modules
default in this case.
The @npm external repository and the fine grained npm package
targets are setup using the `yarn_install` or `npm_install` rule
in your WORKSPACE file:
yarn_install(
name = "npm",
package_json = "//:package.json",
yarn_lock = "//:yarn.lock",
)
""",
default = Label("@npm//typescript:typescript__typings"),
),
"supports_workers": attr.bool(
doc = """Intended for internal use only.
Allows you to disable the Bazel Worker strategy for this library.
Typically used together with the "compiler" setting when using a
non-worker aware compiler binary.""",
default = True,
),
# TODO(alexeagle): reconcile with google3: ts_library rules should
# be portable across internal/external, so we need this attribute
# internally as well.
"tsconfig": attr.label(
doc = """A tsconfig.json file containing settings for TypeScript compilation.
Note that some properties in the tsconfig are governed by Bazel and will be
overridden, such as `target` and `module`.
The default value is set to `//:tsconfig.json` by a macro. This means you must
either:
- Have your `tsconfig.json` file in the workspace root directory
- Use an alias in the root BUILD.bazel file to point to the location of tsconfig:
`alias(name="tsconfig.json", actual="//path/to:tsconfig-something.json")`
- Give an explicit `tsconfig` attribute to all `ts_library` targets
""",
allow_single_file = True,
),
"tsickle_typed": attr.bool(default = True),
"deps": attr.label_list(aspects = local_deps_aspects),
}),
outputs = {
"tsconfig": "%{name}_tsconfig.json",
},
)
"""
`ts_library` type-checks and compiles a set of TypeScript sources to JavaScript.
It produces declarations files (`.d.ts`) which are used for compiling downstream
TypeScript targets and JavaScript for the browser and Closure compiler.
"""
def ts_library_macro(tsconfig = None, **kwargs):
"""Wraps `ts_library` to set the default for the `tsconfig` attribute.
This must be a macro so that the string is converted to a label in the context of the
workspace that declares the `ts_library` target, rather than the workspace that defines
`ts_library`, or the workspace where the build is taking place.
This macro is re-exported as `ts_library` in the public API.
Args:
tsconfig: the label pointing to a tsconfig.json file
**kwargs: remaining args to pass to the ts_library rule
"""
if not tsconfig:
tsconfig = "//:tsconfig.json"
ts_library(tsconfig = tsconfig, **kwargs)