blob: a514e9e66255901774b1358cff7be6162ae30a13 [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")
load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo")
_DEBUG = False
DEPS_ASPECTS = [
module_mappings_aspect,
]
_ADDITIONAL_D_TS = attr.label_list(
allow_files = True,
)
# Mock out the JsInfo blaze-only provider
JsInfo = provider()
# 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 = [JsInfo],
),
"deps": attr.label_list(
aspects = DEPS_ASPECTS,
providers = [DeclarationInfo],
),
"_additional_d_ts": _ADDITIONAL_D_TS,
}
# Attributes shared by any typescript-compatible aspect.
ASPECT_ATTRIBUTES = {
"_additional_d_ts": _ADDITIONAL_D_TS,
}
COMMON_OUTPUTS = {
# Allow the tsconfig.json to be generated without running compile actions.
"tsconfig": "%{name}_tsconfig.json",
}
_DEPSET_TYPE = type(depset())
def _collect_dep_declarations(ctx, declaration_infos):
"""Flattens DeclarationInfo from typescript and javascript dependencies.
Args:
ctx: ctx.
declaration_infos: list of DeclarationInfo collected from dependent targets
Returns:
A struct of depsets for direct, transitive and type-blacklisted declarations.
"""
# .d.ts files from direct dependencies, ok for strict deps
direct_deps_declarations = [dep.declarations for dep in declaration_infos]
# all reachable .d.ts files from dependencies.
transitive_deps_declarations = [dep.transitive_declarations for dep in declaration_infos]
# all reachable .d.ts files from node_modules attribute (if it has a typescript provider)
if hasattr(ctx.attr, "node_modules"):
if DeclarationInfo in ctx.attr.node_modules:
transitive_deps_declarations.append(ctx.attr.node_modules[DeclarationInfo].transitive_declarations)
elif hasattr(ctx.attr.node_modules, "typescript"):
# TODO(b/139705078): remove this case after bazel BUILD file generation for node_modules is updated
transitive_deps_declarations.append([ctx.attr.node_modules.typescript.transitive_declarations])
# .d.ts files whose types tsickle will not emit (used for ts_declaration(generate_externs=False).
type_blacklisted_declarations = [dep.type_blacklisted_declarations for dep in declaration_infos]
# If a tool like github.com/angular/clutz can create .d.ts from type annotated .js
# its output will be collected here.
return DeclarationInfo(
declarations = depset(transitive = direct_deps_declarations),
transitive_declarations = depset(ctx.files._additional_d_ts, transitive = transitive_deps_declarations),
type_blacklisted_declarations = depset(transitive = type_blacklisted_declarations),
)
def _should_generate_externs(ctx):
"""Whether externs should be generated.
If ctx has a generate_externs attribute, the value of that is returned.
Otherwise, this is true."""
return getattr(ctx.attr, "generate_externs", True)
def _get_runtime(ctx):
"""Gets the runtime for the rule.
Defaults to "browser" if the runtime attr isn't present."""
return getattr(ctx.attr, "runtime", "browser")
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 + ".mjs")]
# 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.mjs")]
closure_js_files += [ctx.actions.declare_file(basename + ".ngsummary.mjs")]
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,
declaration_infos = None,
compile_action = None,
devmode_compile_action = None,
jsx_factory = None,
tsc_wrapped_tsconfig = None,
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.
declaration_infos: list of DeclarationInfo. Explicit list of declarations to be used instead of those on 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.
tsconfig: The tsconfig file to output, if other than ctx.outputs.tsconfig.
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
if declaration_infos == None:
if not hasattr(ctx.attr, "deps"):
fail("compile_ts must either be called from a rule with a deps attr, or must be given declaration_infos")
# By default, we collect dependencies from the ctx, when used as a rule
declaration_infos = [
d[DeclarationInfo]
for d in ctx.attr.deps + getattr(ctx.attr, "_helpers", [])
]
tsconfig = tsconfig if tsconfig != None else ctx.outputs.tsconfig
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
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 _get_runtime(ctx) != "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, declaration_infos)
type_blacklisted_declarations = dep_declarations.type_blacklisted_declarations
if not is_library and not _should_generate_externs(ctx):
type_blacklisted_declarations = depset(srcs_files, transitive = [type_blacklisted_declarations])
# 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 = dep_declarations.transitive_declarations.to_list() + srcs_files
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.declarations],
)
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 = 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 = compilation_inputs + [tsconfig]
replay_params = compile_action(
ctx,
inputs,
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,
))
devmode_compile_action(
ctx,
compilation_inputs + [tsconfig_json_es5],
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_declarations])
# both ts_library and ts_declarations generate .mjs 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 declaration_infos:
declarations_depsets.append(dep.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_es6_sources_sets = [es6_sources]
for dep in getattr(ctx.attr, "deps", []):
if hasattr(dep, "typescript"):
transitive_es6_sources_sets += [dep.typescript.transitive_es6_sources]
transitive_es6_sources = depset(transitive = transitive_es6_sources_sets)
declarations_provider = DeclarationInfo(
declarations = depset(transitive = declarations_depsets),
transitive_declarations = transitive_decls,
type_blacklisted_declarations = type_blacklisted_declarations,
)
return {
"providers": [
DefaultInfo(
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,
),
files = depset(transitive = files_depsets),
),
OutputGroupInfo(
es5_sources = es5_sources,
es6_sources = es6_sources,
),
declarations_provider,
],
# Also expose the DeclarationInfo as a named provider so that aspect implementations can reference it
# Otherwise they would be forced to reference it by a numeric index out of the "providers" list above.
"declarations": declarations_provider,
"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": getattr(ctx.attr, "module_name", None),
# Expose the tags so that a Skylark aspect can access them.
"tags": ctx.attr.tags if hasattr(ctx.attr, "tags") else ctx.rule.attr.tags,
"typescript": {
"devmode_manifest": devmode_manifest,
"es5_sources": es5_sources,
"es6_sources": es6_sources,
"replay_params": replay_params,
"transitive_es6_sources": transitive_es6_sources,
"tsickle_externs": tsickle_externs,
},
}
def ts_providers_dict_to_struct(d):
""" 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.
Args:
d: the dict to convert
Returns:
An immutable struct created from the input dict
"""
# These keys are present in the dict so that aspects can reference them,
# however they should not be output as legacy providers since we have modern
# symbol-typed providers for them.
js_provider = d.pop("js", None)
declarations_provider = d.pop("declarations", None)
# Promote the "js" string-typed provider to a modern provider
if js_provider:
# Create a new providers list rather than modify the existing list
d["providers"] = d.get("providers", []) + [js_provider]
for key, value in d.items():
if key != "output_groups" and type(value) == type({}):
d[key] = struct(**value)
result = struct(**d)
# Restore the elements we removed, to avoid side-effect of mutating the argument
if js_provider:
d["js"] = js_provider
if declarations_provider:
d["declarations"] = declarations_provider
return result