| # 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, |
| ] |
| |
| _ADDITIONAL_D_TS = attr.label_list( |
| allow_files = True, |
| ) |
| |
| # 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": _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", |
| } |
| |
| # 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 |
| ] |
| |
| # all reachable .d.ts files from node_modules attribute (if it has a typescript provider) |
| if hasattr(ctx.attr, "node_modules") and hasattr(ctx.attr.node_modules, "typescript"): |
| transitive_deps_declarations += [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.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 _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 + ".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, |
| 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. |
| 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 |
| deps = deps if deps != None else ctx.attr.deps |
| 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 |
| |
| # 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 _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, deps) |
| input_declarations = depset(src_declarations, transitive = [dep_declarations.transitive]) |
| type_blacklisted_declarations = dep_declarations.type_blacklisted |
| if not is_library and not _should_generate_externs(ctx): |
| 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 = dep_declarations.transitive.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.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 = 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]) |
| |
| # 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_es6_sources = depset() |
| for dep in deps: |
| if hasattr(dep, "typescript"): |
| transitive_es6_sources = depset(transitive = [ |
| transitive_es6_sources, |
| dep.typescript.transitive_es6_sources, |
| ]) |
| transitive_es6_sources = depset(transitive = [transitive_es6_sources, es6_sources]) |
| |
| 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), |
| ), |
| ], |
| "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), |
| "output_groups": { |
| "es5_sources": es5_sources, |
| "es6_sources": es6_sources, |
| }, |
| # 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, |
| # 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_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) |