blob: 8bb4732dd2a46674169c06124150de136478738a [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.
"""Used for compilation by the different implementations of build_defs.bzl.
"""
load(":common/json_marshal.bzl", "json_marshal")
load(":common/module_mappings.bzl", "module_mappings_aspect")
_DEBUG = False
DEPS_ASPECTS = [
module_mappings_aspect,
]
# Attributes shared by any typescript-compatible rule (ts_library, ng_module)
COMMON_ATTRIBUTES = {
"data": attr.label_list(
default = [],
allow_files = True,
),
# A list of diagnostics expected when compiling this library, in the form of
# "diagnostic:regexp", e.g. "TS1234:failed to quizzle the .* wobble".
# Useful to test for expected compilation errors.
"expected_diagnostics": attr.string_list(),
# Whether to generate externs.js from any "declare" statement.
"generate_externs": attr.bool(default = True),
# Used to determine module mappings
"module_name": attr.string(),
"module_root": attr.string(),
# TODO(evanm): make this the default and remove the option.
"runtime": attr.string(default = "browser"),
# TODO(radokirov): remove this attr when clutz is stable enough to consume
# any closure JS code.
"runtime_deps": attr.label_list(
default = [],
providers = ["js"],
),
"deps": attr.label_list(aspects = DEPS_ASPECTS),
"_additional_d_ts": attr.label_list(
allow_files = True,
),
}
COMMON_OUTPUTS = {
# Allow the tsconfig.json to be generated without running compile actions.
"tsconfig": "%{name}_tsconfig.json",
}
# TODO(plf): Enforce this at analysis time.
def assert_js_or_typescript_deps(ctx, deps = None):
# `deps` args is optinal for backward compat.
# Fallback to `ctx.attr.deps`.
deps = deps if deps != None else ctx.attr.deps
for dep in deps:
if not hasattr(dep, "typescript") and not hasattr(dep, "js"):
allowed_deps_msg = "Dependencies must be ts_library"
fail("%s is neither a TypeScript nor a JS producing rule.\n%s\n" % (dep.label, allowed_deps_msg))
_DEPSET_TYPE = type(depset())
def _check_ts_provider(dep):
"""Verifies the type shape of the typescript provider in dep, if it has one.
"""
# Under Bazel, some third parties have created typescript providers which may not be compatible.
# Rather than users getting an obscure error later, explicitly check them and point to the
# target that created the bad provider.
# TODO(alexeagle): remove this after some transition period, maybe mid-2019
if hasattr(dep, "typescript"):
if type(dep.typescript.declarations) != _DEPSET_TYPE:
fail("typescript provider in %s defined declarations as a %s rather than a depset" % (
dep.label,
type(dep.typescript.declarations),
))
if type(dep.typescript.transitive_declarations) != _DEPSET_TYPE:
fail("typescript provider in %s defined transitive_declarations as a %s rather than a depset" % (
dep.label,
type(dep.typescript.transitive_declarations),
))
if type(dep.typescript.type_blacklisted_declarations) != _DEPSET_TYPE:
fail("typescript provider in %s defined type_blacklisted_declarations as a %s rather than a depset" % (
dep.label,
type(dep.typescript.type_blacklisted_declarations),
))
return dep
def _collect_dep_declarations(ctx, deps):
"""Collects .d.ts files from typescript and javascript dependencies.
Args:
ctx: ctx.
deps: dependent targets, generally ctx.attr.deps
Returns:
A struct of depsets for direct, transitive and type-blacklisted declarations.
"""
deps_and_helpers = [
_check_ts_provider(dep)
for dep in deps + getattr(ctx.attr, "_helpers", [])
if hasattr(dep, "typescript")
]
# .d.ts files from direct dependencies, ok for strict deps
direct_deps_declarations = [dep.typescript.declarations for dep in deps_and_helpers]
# all reachable .d.ts files from dependencies.
transitive_deps_declarations = [
dep.typescript.transitive_declarations
for dep in deps_and_helpers
]
# .d.ts files whose types tsickle will not emit (used for ts_declaration(generate_externs=False).
type_blacklisted_declarations = [
dep.typescript.type_blacklisted_declarations
for dep in deps_and_helpers
]
# If a tool like github.com/angular/clutz can create .d.ts from type annotated .js
# its output will be collected here.
return struct(
direct = depset(transitive = direct_deps_declarations),
transitive = depset(
[extra for extra in ctx.files._additional_d_ts],
transitive = transitive_deps_declarations,
),
type_blacklisted = depset(transitive = type_blacklisted_declarations),
)
def _outputs(ctx, label, srcs_files = []):
"""Returns closure js, devmode js, and .d.ts output files.
Args:
ctx: ctx.
label: Label. package label.
srcs_files: File list. sources files list.
Returns:
A struct of file lists for different output types.
"""
workspace_segments = label.workspace_root.split("/") if label.workspace_root else []
package_segments = label.package.split("/") if label.package else []
trim = len(workspace_segments) + len(package_segments)
create_shim_files = False
closure_js_files = []
devmode_js_files = []
declaration_files = []
for input_file in srcs_files:
is_dts = input_file.short_path.endswith(".d.ts")
if is_dts and not create_shim_files:
continue
basename = "/".join(input_file.short_path.split("/")[trim:])
for ext in [".d.ts", ".tsx", ".ts"]:
if basename.endswith(ext):
basename = basename[:-len(ext)]
break
closure_js_files += [ctx.actions.declare_file(basename + ".closure.js")]
# Temporary until all imports of ngfactory/ngsummary files are removed
# TODO(alexeagle): clean up after Ivy launch
if getattr(ctx, "compile_angular_templates", False):
closure_js_files += [ctx.actions.declare_file(basename + ".ngfactory.closure.js")]
closure_js_files += [ctx.actions.declare_file(basename + ".ngsummary.closure.js")]
if not is_dts:
devmode_js_files += [ctx.actions.declare_file(basename + ".js")]
declaration_files += [ctx.actions.declare_file(basename + ".d.ts")]
# Temporary until all imports of ngfactory/ngsummary files are removed
# TODO(alexeagle): clean up after Ivy launch
if getattr(ctx, "compile_angular_templates", False):
devmode_js_files += [ctx.actions.declare_file(basename + ".ngfactory.js")]
devmode_js_files += [ctx.actions.declare_file(basename + ".ngsummary.js")]
return struct(
closure_js = closure_js_files,
devmode_js = devmode_js_files,
declarations = declaration_files,
)
def compile_ts(
ctx,
is_library,
srcs = None,
deps = None,
compile_action = None,
devmode_compile_action = None,
jsx_factory = None,
tsc_wrapped_tsconfig = None,
outputs = _outputs):
"""Creates actions to compile TypeScript code.
This rule is shared between ts_library and ts_declaration.
Args:
ctx: ctx.
is_library: boolean. False if only compiling .dts files.
srcs: label list. Explicit list of sources to be used instead of ctx.attr.srcs.
deps: label list. Explicit list of deps to be used instead of ctx.attr.deps.
compile_action: function. Creates the compilation action.
devmode_compile_action: function. Creates the compilation action
for devmode.
jsx_factory: optional string. Enables overriding jsx pragma.
tsc_wrapped_tsconfig: function that produces a tsconfig object.
outputs: function from a ctx to the expected compilation outputs.
Returns:
struct that will be returned by the rule implementation.
"""
### Collect srcs and outputs.
srcs = srcs if srcs != None else ctx.attr.srcs
deps = deps if deps != None else ctx.attr.deps
srcs_files = [f for t in srcs for f in t.files.to_list()]
src_declarations = [] # d.ts found in inputs.
tsickle_externs = [] # externs.js generated by tsickle, if any.
has_sources = False
# Validate the user inputs.
assert_js_or_typescript_deps(ctx, deps)
for src in srcs:
if src.label.package != ctx.label.package:
# Sources can be in sub-folders, but not in sub-packages.
fail("Sources must be in the same package as the ts_library rule, " +
"but %s is not in %s" % (src.label, ctx.label.package), "srcs")
if hasattr(src, "typescript"):
# Guard against users accidentally putting deps into srcs by
# rejecting all srcs values that have a TypeScript provider.
# TS rules produce a ".d.ts" file, which is a valid input in "srcs",
# and will then be compiled as a source .d.ts file would, creating
# externs etc.
fail(
"must not reference any TypeScript rules - did you mean deps?",
"srcs",
)
for f in src.files.to_list():
has_sources = True
if not is_library and not f.path.endswith(".d.ts"):
fail("srcs must contain only type declarations (.d.ts files), " +
"but %s contains %s" % (src.label, f.short_path), "srcs")
if f.path.endswith(".d.ts"):
src_declarations += [f]
continue
outs = outputs(ctx, ctx.label, srcs_files)
transpiled_closure_js = outs.closure_js
transpiled_devmode_js = outs.devmode_js
gen_declarations = outs.declarations
if has_sources and ctx.attr.runtime != "nodejs":
# Note: setting this variable controls whether tsickle is run at all.
tsickle_externs = [ctx.actions.declare_file(ctx.label.name + ".externs.js")]
dep_declarations = _collect_dep_declarations(ctx, deps)
input_declarations = depset(src_declarations, transitive = [dep_declarations.transitive])
type_blacklisted_declarations = dep_declarations.type_blacklisted
if not is_library and not ctx.attr.generate_externs:
type_blacklisted_declarations += srcs_files
# The depsets of output files. These are the files that are always built
# (including e.g. if you "blaze build :the_target" directly).
files_depsets = []
# A manifest listing the order of this rule's *.ts files (non-transitive)
# Only generated if the rule has any sources.
devmode_manifest = None
# Enable to produce a performance trace when compiling TypeScript to JS.
# The trace file location will be printed as a build result and can be read
# in Chrome's chrome://tracing/ UI.
perf_trace = _DEBUG
if "TYPESCRIPT_PERF_TRACE_TARGET" in ctx.var:
perf_trace = str(ctx.label) == ctx.var["TYPESCRIPT_PERF_TRACE_TARGET"]
compilation_inputs = depset(srcs_files, transitive = [input_declarations])
tsickle_externs_path = tsickle_externs[0] if tsickle_externs else None
# Calculate allowed dependencies for strict deps enforcement.
allowed_deps = depset(
# A target's sources may depend on each other,
srcs_files,
# or on a .d.ts from a direct dependency
transitive = [dep_declarations.direct],
)
tsconfig_es6 = tsc_wrapped_tsconfig(
ctx,
compilation_inputs,
srcs_files,
jsx_factory = jsx_factory,
tsickle_externs = tsickle_externs_path,
type_blacklisted_declarations = type_blacklisted_declarations.to_list(),
allowed_deps = allowed_deps,
)
# Do not produce declarations in ES6 mode, tsickle cannot produce correct
# .d.ts (or even errors) from the altered Closure-style JS emit.
tsconfig_es6["compilerOptions"]["declaration"] = False
tsconfig_es6["compilerOptions"].pop("declarationDir")
outputs = transpiled_closure_js + tsickle_externs
node_profile_args = []
if perf_trace and has_sources:
perf_trace_file = ctx.actions.declare_file(ctx.label.name + ".es6.trace")
tsconfig_es6["bazelOptions"]["perfTracePath"] = perf_trace_file.path
outputs.append(perf_trace_file)
profile_file = ctx.actions.declare_file(ctx.label.name + ".es6.v8.log")
node_profile_args = [
"--prof",
# Without nologfile_per_isolate, v8 embeds an
# unpredictable hash code in the file name, which
# doesn't work with blaze.
"--nologfile_per_isolate",
"--logfile=" + profile_file.path,
]
outputs.append(profile_file)
files_depsets.append(depset([perf_trace_file, profile_file]))
ctx.actions.write(
output = ctx.outputs.tsconfig,
content = json_marshal(tsconfig_es6),
)
# Parameters of this compiler invocation in case we need to replay this with different
# settings.
replay_params = None
if has_sources:
inputs = depset([ctx.outputs.tsconfig], transitive = [compilation_inputs])
replay_params = compile_action(
ctx,
inputs,
outputs,
ctx.outputs.tsconfig,
node_profile_args,
)
devmode_manifest = ctx.actions.declare_file(ctx.label.name + ".es5.MF")
tsconfig_json_es5 = ctx.actions.declare_file(ctx.label.name + "_es5_tsconfig.json")
outputs = (
transpiled_devmode_js + gen_declarations + [devmode_manifest]
)
tsconfig_es5 = tsc_wrapped_tsconfig(
ctx,
compilation_inputs,
srcs_files,
jsx_factory = jsx_factory,
devmode_manifest = devmode_manifest.path,
allowed_deps = allowed_deps,
)
node_profile_args = []
if perf_trace:
perf_trace_file = ctx.actions.declare_file(ctx.label.name + ".es5.trace")
tsconfig_es5["bazelOptions"]["perfTracePath"] = perf_trace_file.path
outputs.append(perf_trace_file)
profile_file = ctx.actions.declare_file(ctx.label.name + ".es5.v8.log")
node_profile_args = [
"--prof",
# Without nologfile_per_isolate, v8 embeds an
# unpredictable hash code in the file name, which
# doesn't work with blaze.
"--nologfile_per_isolate",
"--logfile=" + profile_file.path,
]
outputs.append(profile_file)
files_depsets.append(depset([perf_trace_file, profile_file]))
ctx.actions.write(output = tsconfig_json_es5, content = json_marshal(
tsconfig_es5,
))
inputs = depset([tsconfig_json_es5], transitive = [compilation_inputs])
devmode_compile_action(
ctx,
inputs,
outputs,
tsconfig_json_es5,
node_profile_args,
)
# TODO(martinprobst): Merge the generated .d.ts files, and enforce strict
# deps (do not re-export transitive types from the transitive closure).
transitive_decls = depset(src_declarations + gen_declarations, transitive = [dep_declarations.transitive])
# both ts_library and ts_declarations generate .closure.js files:
# - for libraries, this is the ES6/production code
# - for declarations, these are generated shims
es6_sources = depset(transpiled_closure_js + tsickle_externs)
if is_library:
es5_sources = depset(transpiled_devmode_js)
else:
# In development mode, no code ever references shims as they only
# contain types, and the ES5 code does not get type annotated.
es5_sources = depset(tsickle_externs)
# Similarly, in devmode these sources do not get loaded, so do not need
# to be in a manifest.
devmode_manifest = None
# Downstream rules see the .d.ts files produced or declared by this rule.
declarations_depsets = [depset(gen_declarations + src_declarations)]
if not srcs_files:
# Re-export sources from deps.
# TODO(b/30018387): introduce an "exports" attribute.
for dep in deps:
if hasattr(dep, "typescript"):
declarations_depsets.append(dep.typescript.declarations)
files_depsets.extend(declarations_depsets)
# If this is a ts_declaration, add tsickle_externs to the outputs list to
# force compilation of d.ts files. (tsickle externs are produced by running a
# compilation over the d.ts file and extracting type information.)
if not is_library:
files_depsets.append(depset(tsickle_externs))
transitive_es5_sources = depset()
transitive_es6_sources = depset()
for dep in deps:
if hasattr(dep, "typescript"):
transitive_es5_sources = depset(transitive = [
transitive_es5_sources,
dep.typescript.transitive_es5_sources,
])
transitive_es6_sources = depset(transitive = [
transitive_es6_sources,
dep.typescript.transitive_es6_sources,
])
transitive_es5_sources = depset(transitive = [transitive_es5_sources, es5_sources])
transitive_es6_sources = depset(transitive = [transitive_es6_sources, es6_sources])
return {
"files": depset(transitive = files_depsets),
"instrumented_files": {
"dependency_attributes": ["deps", "runtime_deps"],
"extensions": ["ts"],
"source_attributes": ["srcs"],
},
# Expose the module_name so that packaging rules can access it.
# e.g. rollup_bundle under Bazel needs to convert this into a UMD global
# name in the Rollup configuration.
"module_name": ctx.attr.module_name,
"output_groups": {
"es5_sources": es5_sources,
"es6_sources": es6_sources,
},
"runfiles": ctx.runfiles(
# Note: don't include files=... here, or they will *always* be built
# by any dependent rule, regardless of whether it needs them.
# But these attributes are needed to pass along any input runfiles:
collect_default = True,
collect_data = True,
),
# Expose the tags so that a Skylark aspect can access them.
"tags": ctx.attr.tags,
# TODO(martinprobst): Prune transitive deps, only re-export what's needed.
"typescript": {
"declarations": depset(transitive = declarations_depsets),
"devmode_manifest": devmode_manifest,
"es5_sources": es5_sources,
"es6_sources": es6_sources,
"replay_params": replay_params,
"transitive_declarations": transitive_decls,
"transitive_es5_sources": transitive_es5_sources,
"transitive_es6_sources": transitive_es6_sources,
"tsickle_externs": tsickle_externs,
"type_blacklisted_declarations": type_blacklisted_declarations,
},
}
# Converts a dict to a struct, recursing into a single level of nested dicts.
# This allows users of compile_ts to modify or augment the returned dict before
# converting it to an immutable struct.
def ts_providers_dict_to_struct(d):
for key, value in d.items():
if key != "output_groups" and type(value) == type({}):
d[key] = struct(**value)
return struct(**d)