| # 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 = "@build_bazel_rules_typescript//:@bazel/typescript/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 |
| |
| # 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, |
| ), |
| "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"), |
| ), |
| "compile_angular_templates": attr.bool( |
| doc = """Run the Angular ngtsc compiler under ts_library""", |
| ), |
| "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) |