diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..f6cb8ad9 --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Google diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6c50b7cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +node_modules +/bazel-* diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..8f95963a --- /dev/null +++ b/AUTHORS @@ -0,0 +1,9 @@ +# This the official list of Bazel authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as: +# Name or Organization +# The email address is not required for organizations. + +Google Inc. diff --git a/BUILD b/BUILD new file mode 100644 index 00000000..8ec86ceb --- /dev/null +++ b/BUILD @@ -0,0 +1,22 @@ +# 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. + +# The node_modules directory is created by `yarn install` +# WORKAROUND for https://github.com/bazelbuild/bazel/issues/374#issuecomment-296217940 +# see notes in internal/yarn_install.bzl +filegroup( + name = "node_modules", + srcs = glob(["node_modules/**/*"]), + visibility = ["//visibility:public"], +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..745cbdcb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +Want to contribute? Great! First, read this page (including the small print at the end). + +### Before you contribute +**Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) +(CLA)**, which you can do online. + +The CLA is necessary mainly because you own the copyright to your changes, +even after your contribution becomes part of our codebase, so we need your +permission to use and distribute your code. We also need to be sure of +various other things for instance that you'll tell us if you know that +your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch +with us first. Use the issue tracker to explain your idea so we can help and +possibly guide you. + +### Code reviews and other contributions. +**All submissions, including submissions by project members, require review.** +TODO(alexeagle): document how external contributions are handled. + +### The small print +Contributions made by corporations are covered by a different agreement than +the one above, the +[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 00000000..174d3880 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,12 @@ +# People who have agreed to one of the CLAs and can contribute patches. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# https://developers.google.com/open-source/cla/individual +# https://developers.google.com/open-source/cla/corporate +# +# Names should be added to this file as: +# Name + +Alex Eagle diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..f895085c --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# TypeScript rules for Bazel + +**WARNING: this is an early release with limited features. Breaking changes are likely. Not recommended for general use.** + +The TypeScript rules integrate the TypeScript compiler with Bazel. + +## Installation + +First, install a current Bazel distribution. + +Create a `BUILD` file in your project root: + +```python +package(default_visibility = ["//visibility:public"]) +exports_files(["tsconfig.json"]) + +# NOTE: this will move to node_modules/BUILD in a later release +filegroup(name = "node_modules", srcs = glob(["node_modules/**/*"])) +``` + +> Note, on Mac file paths are case-insensitive, so make sure there isn't already +a `build` folder in the project root. + +Next create a `WORKSPACE` file in your project root (or edit the existing one) +containing: + +```python +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") + +git_repository( + name = "io_bazel_rules_typescript", + remote = "sso://bazel/rules_typescript", + commit = "b25f535a7fdb4382dc67ed455c54b7ba23995c85") + +load("@io_bazel_rules_typescript//:defs.bzl", "node_repositories", "yarn_install") + +node_repositories() +yarn_install(package_json = "//:package.json") + +``` + +## Usage + +Currently, the only available rule is `ts_library` which invokes the TypeScript +compiler on one compilation unit (generally one directory of source files). + +Create a `BUILD` file next to your sources: + +``` +package(default_visibility=["//visibility:public"]) +load("@io_bazel_rules_typescript//:defs.bzl", "ts_library") + +ts_library( + name = "my_code", + srcs = glob(["*.ts"]), + deps = ["//path/to/other:library"], + tsconfig = "//:tsconfig.json", +) +``` + +(Note that you may want to name the ts_library target the same as the enclosing +directory, making it the default target in the package.) + +Then build it: + +`bazel build //path/to/package:target` + +The resulting `.d.ts` file paths will be printed. Additionally, the `.js` +outputs from TypeScript will be written to disk, next to the `.d.ts` files. + +## Notes + +If you'd like a "watch mode", try https://github.com/bazelbuild/bazel-watcher +(note, it's also quite new). + +At some point, we plan to release a tool similar to [gazelle] to generate the +BUILD files from your source code. + +[gazelle]: https://github.com/bazelbuild/rules_go/tree/master/go/tools/gazelle diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 00000000..0d08b8fa --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,26 @@ +# 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. + +workspace(name = "io_bazel_rules_typescript") + +load("//:defs.bzl", "node_repositories", "yarn_install") + +# Install a hermetic version of node. +# After this is run, label @io_bazel_rules_typescript_node//:bin/node will exist +node_repositories() + +# Install yarn, and run yarn install to create node_modules. +# After this is run, label @yarn//installed:node_modules will exist. +# (But your rules can reference //:node_modules instead) +yarn_install(package_json = "//:package.json") diff --git a/defs.bzl b/defs.bzl new file mode 100644 index 00000000..6023524f --- /dev/null +++ b/defs.bzl @@ -0,0 +1,21 @@ +# 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. + +""" Public API surface is re-exported here. + +Users should not load files under "/internal" +""" +load("//internal:build_defs.bzl", "ts_library") +load("//internal:node_install.bzl", "node_repositories") +load("//internal:yarn_install.bzl", "yarn_install") diff --git a/examples/BUILD b/examples/BUILD new file mode 100644 index 00000000..051e4a18 --- /dev/null +++ b/examples/BUILD @@ -0,0 +1,30 @@ +# 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. + +package(default_visibility = ["//visibility:public"]) + +load("//:defs.bzl", "ts_library") + +ts_library( + name = "foo_ts_library", + srcs = ["foo.ts"], + tsconfig = ":tsconfig.json", +) + +ts_library( + name = "bar_ts_library", + srcs = ["bar.ts"], + tsconfig = ":tsconfig.json", + deps = [":foo_ts_library"], +) \ No newline at end of file diff --git a/examples/bar.ts b/examples/bar.ts new file mode 100644 index 00000000..50ddbb79 --- /dev/null +++ b/examples/bar.ts @@ -0,0 +1 @@ +import {greeter} from './foo'; \ No newline at end of file diff --git a/examples/foo.ts b/examples/foo.ts new file mode 100644 index 00000000..391dec42 --- /dev/null +++ b/examples/foo.ts @@ -0,0 +1,26 @@ +/** + * @license + * 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. + */ + +export class Greeter { + constructor(public greeting: string) {} + greet() { + return '

' + this.greeting + '

'; + } +}; + +export const greeter = new Greeter('Hello, world!'); +document.body.innerHTML = greeter.greet(); diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 00000000..a83e8e65 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": true + } +} + diff --git a/internal/BUILD b/internal/BUILD new file mode 100644 index 00000000..33605dda --- /dev/null +++ b/internal/BUILD @@ -0,0 +1,24 @@ +# 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. + +package(default_visibility = ["//visibility:public"]) + +load(":node.bzl", "node_binary") + +node_binary( + name = "tsc_wrapped", + data = ["@yarn//installed:node_modules"], + # TODO(alexeagle): plug in tsc_wrapped.js instead + main = "yarn/installed/node_modules/typescript/lib/tsc.js", +) diff --git a/internal/build_defs.bzl b/internal/build_defs.bzl new file mode 100644 index 00000000..2caffe03 --- /dev/null +++ b/internal/build_defs.bzl @@ -0,0 +1,173 @@ +# 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 rules. +""" +# pylint: disable=unused-argument +# pylint: disable=missing-docstring +load("//internal:common/compilation.bzl", "compile_ts") +load("//internal:executables.bzl", "get_tsc", "get_node") +load("//internal:common/json_marshal.bzl", "json_marshal") +load("//internal:common/tsconfig.bzl", "create_tsconfig") + +def _compile_action(ctx, inputs, outputs, config_file_path): + externs_files = [] + non_externs_files = [] + for output in outputs: + if output.basename.endswith(".externs.js"): + externs_files.append(output) + elif output.basename.endswith(".es5.MF"): + ctx.file_action(output, content="") + else: + non_externs_files.append(output) + + # TODO(plf): For now we mock creation of files other than {name}.js. + for externs_file in externs_files: + ctx.file_action(output=externs_file, content="") + + ctx.action( + inputs=inputs + [ctx.file.tsconfig], + outputs=non_externs_files, + arguments=["-p", config_file_path], + executable=ctx.executable._tsc) + + +def _devmode_compile_action(ctx, inputs, outputs, config_file_path): + _compile_action(ctx, inputs, outputs, config_file_path) + +def _tsc_wrapped_tsconfig(ctx, + files, + srcs, + devmode_manifest=None, + tsickle_externs=None, + type_blacklisted_declarations=[], + allowed_deps=set(), + jsx_factory=None, + ngc_out=[]): + variant = "" + if devmode_manifest: variant += "_es5" + tsconfig_json = ctx.new_file(ctx.label.name + variant + "_tsconfig.json") + + # 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. + + workspace_path = "/".join([".."] * len(tsconfig_json.dirname.split("/"))) + host_bin = "bazel-out/host/bin" + + if ctx.workspace_name == "io_bazel_rules_typescript": + runfiles = "internal/tsc_wrapped.runfiles" + else: + runfiles = "external/io_bazel_rules_typescript/internal/tsc_wrapped.runfiles" + + config = create_tsconfig(ctx, files, srcs, tsconfig_json.dirname, + devmode_manifest=devmode_manifest) + + config["compilerOptions"].update({ + "typeRoots": ["/".join([ + workspace_path, host_bin, runfiles, + "yarn/installed/node_modules/@types"] + )], + "baseUrl": workspace_path, + "paths": { + "*": [ + "/".join([host_bin, runfiles, "yarn/installed/node_modules/*"]), + "/".join([host_bin, runfiles, "yarn/installed/node_modules/@types/*"]), + ], + # Workaround https://github.com/Microsoft/TypeScript/issues/15962 + # Needed for Angular to build with Bazel. + # TODO(alexeagle): fix the bug upstream or find a better place for + # this workaround. + "zone.js": [ + "/".join([host_bin, runfiles, "yarn/installed/node_modules/zone.js/dist/zone.js.d.ts"]), + ] + }, + }) + + # 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: + config["extends"] = "{}/{}".format(workspace_path, ctx.file.tsconfig.path[:-5]) + + ctx.file_action(output=tsconfig_json, content=json_marshal(config)) + return tsconfig_json + +# ************ # +# 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. + """ + ctx.file_action( + output = ctx.outputs._js_typings, + content = "") + + return compile_ts(ctx, is_library=True, compile_action=_compile_action, + devmode_compile_action=_devmode_compile_action, + tsc_wrapped_tsconfig=_tsc_wrapped_tsconfig) + +ts_library = rule( + _ts_library_impl, + attrs={ + "srcs": + attr.label_list( + allow_files=FileType([ + ".ts", + ".tsx", + ]), + mandatory=True,), + "deps": + attr.label_list(), + # TODO(evanm): make this the default and remove the option. + "runtime": + attr.string(default="browser"), + # 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(allow_files = True, single_file=True), + "_additional_d_ts": + attr.label_list(), + "_tsc": + attr.label( + default=get_tsc(), + single_file=False, + allow_files=True, + executable=True, + cfg="host",), + "_node": + attr.label( + default=get_node(), + single_file=True, + allow_files=True, + executable=True, + cfg="host",), + "_node_modules": attr.label(default = Label("@yarn//installed:node_modules")) + }, + fragments=["js"], + outputs={"_js_typings": "_%{name}_js_typings.d.ts"},) diff --git a/internal/common/compilation.bzl b/internal/common/compilation.bzl new file mode 100644 index 00000000..9a684397 --- /dev/null +++ b/internal/common/compilation.bzl @@ -0,0 +1,222 @@ +# 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. +""" + +# TODO(plf): Enforce this at analysis time. +def assert_js_or_typescript_deps(ctx): + for dep in ctx.attr.deps: + if not hasattr(dep, "typescript") and not hasattr(dep, "js"): + fail( + ("%s is neither a TypeScript nor a JS producing rule." % dep.label) + + "\nDependencies must be ts_library, ts_declaration, or " + + # TODO(plf): Leaving this here for now, but this message does not + # make sense in opensource. + "JavaScript library rules (js_library, pinto_library, etc, but " + + "also proto_library, soy_js).\n") + +def _collect_transitive_dts(ctx): + all_deps_declarations = set() + type_blacklisted_declarations = set() + for extra in ctx.files._additional_d_ts: + all_deps_declarations += set([extra]) + for dep in ctx.attr.deps: + if hasattr(dep, "typescript"): + all_deps_declarations += dep.typescript.transitive_declarations + type_blacklisted_declarations += ( + dep.typescript.type_blacklisted_declarations) + return struct( + transitive_declarations=list(all_deps_declarations), + type_blacklisted_declarations=list(type_blacklisted_declarations) + ) + +def _outputs(ctx, label, input_file): + """Returns closure js, devmode js, and .d.ts output files for |input_file|. + + Args: + ctx: ctx. + label: Label. package label. + input_file: File. the input_file + Returns: + A three-tuple of files (.closure.js, .js, .d.ts). + """ + dot = input_file.short_path.rfind(".") + beginning = len(label.package) + if label.package: + beginning += 1 + basename = input_file.short_path[beginning:dot] + return (ctx.new_file(basename + ".closure.js"), + ctx.new_file(basename + ".js"), + ctx.new_file(basename + ".d.ts")) + +def compile_ts(ctx, + is_library, + extra_dts_files=[], + compile_action=None, + devmode_compile_action=None, + jsx_factory=None, + tsc_wrapped_tsconfig=None): + """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. + extra_dts_files: list. Additional dts files to pass for compilation, + not included in the transitive closure of declarations. + 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. + Returns: + struct that will be returned by the rule implementation. + """ + assert_js_or_typescript_deps(ctx) + + ### Collect srcs and outputs. + srcs = ctx.files.srcs + transpiled_closure_js = [] + transpiled_devmode_js = [] + src_declarations = [] # d.ts found in inputs. + gen_declarations = [] # d.ts generated by the TypeScript compiler. + tsickle_externs = [] # externs.js generated by tsickle, if any. + has_sources = False + + # Compile the sources, if any. (For a ts_declaration rule this will + # type-check the d.ts sources and potentially generate externs.) + for src in ctx.attr.srcs: + # 'x/y.ts' ==> 'x/y.js' + 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") + + for f in src.files: + has_sources = True + if is_library: + if f.path.endswith(".d.ts"): + fail("srcs must not contain any type declarations (.d.ts files), " + + "but %s contains %s" % (src.label, f.short_path), "srcs") + outs = _outputs(ctx, src.label, f) + transpiled_closure_js += [outs[0]] + transpiled_devmode_js += [outs[1]] + gen_declarations += [outs[2]] + else: + if 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") + src_declarations += [f] + + if has_sources and ctx.attr.runtime != "nodejs": + # Note: setting this variable controls whether sickle is run at all. + tsickle_externs = [ctx.new_file(ctx.label.name + ".externs.js")] + + transitive_dts = _collect_transitive_dts(ctx) + input_declarations = transitive_dts.transitive_declarations + src_declarations + type_blacklisted_declarations = transitive_dts.type_blacklisted_declarations + if not is_library and not ctx.attr.generate_externs: + type_blacklisted_declarations += ctx.files.srcs + + # A manifest listing the order of this rule's *.ts files (non-transitive) + # Only generated if the rule has any sources. + devmode_manifest = None + + if has_sources: + compilation_inputs = input_declarations + extra_dts_files + srcs + tsickle_externs_path = tsickle_externs[0] if tsickle_externs else None + + # Calculate allowed dependencies for strict deps enforcement. + allowed_deps = srcs # A target's sources may depend on each other. + for dep in ctx.attr.deps: + if hasattr(dep, "typescript"): + allowed_deps += dep.typescript.declarations + allowed_deps += extra_dts_files + + tsconfig_json_es6 = tsc_wrapped_tsconfig( + ctx, + compilation_inputs, + srcs, + jsx_factory=jsx_factory, + tsickle_externs=tsickle_externs_path, + type_blacklisted_declarations=type_blacklisted_declarations, + allowed_deps=allowed_deps) + + inputs = compilation_inputs + [tsconfig_json_es6] + outputs = transpiled_closure_js + tsickle_externs + compile_action(ctx, inputs, outputs, tsconfig_json_es6.path) + + devmode_manifest = ctx.new_file(ctx.label.name + ".es5.MF") + tsconfig_json_es5 = tsc_wrapped_tsconfig( + ctx, + compilation_inputs, + srcs, + jsx_factory=jsx_factory, + devmode_manifest=devmode_manifest, + allowed_deps=allowed_deps) + inputs = compilation_inputs + [tsconfig_json_es5] + outputs = ( + transpiled_devmode_js + gen_declarations + [devmode_manifest]) + devmode_compile_action(ctx, inputs, outputs, tsconfig_json_es5.path) + + # TODO(martinprobst): Merge the generated .d.ts files, and enforce strict + # deps (do not re-export transitive types from the transitive closure). + transitive_decls = input_declarations + gen_declarations + + if is_library: + es6_sources = set(transpiled_closure_js + tsickle_externs) + es5_sources = set(transpiled_devmode_js) + else: + es6_sources = set(tsickle_externs) + es5_sources = set(tsickle_externs) + devmode_manifest = None + + # Downstream rules see the .d.ts files produced or declared by this rule + declarations = gen_declarations + src_declarations + + if not srcs: + # Re-export sources from deps. + # TODO(b/30018387): introduce an "exports" attribute. + for dep in ctx.attr.deps: + if hasattr(dep, "typescript"): + declarations += dep.typescript.declarations + + return struct( + files=set(declarations), + 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, + ), + # TODO(martinprobst): Prune transitive deps, only re-export what's needed. + typescript=struct( + declarations=declarations, + transitive_declarations=transitive_decls, + es6_sources=es6_sources, + es5_sources=es5_sources, + devmode_manifest=devmode_manifest, + js_typings=ctx.outputs._js_typings, + type_blacklisted_declarations=type_blacklisted_declarations, + tsickle_externs=tsickle_externs, + ), + # Expose the tags so that a Skylark aspect can access them. + tags=ctx.attr.tags, + instrumented_files=struct( + extensions=["ts"], + source_attributes=["srcs"], + dependency_attributes=["deps", "runtime_deps"])) diff --git a/internal/common/json_marshal.bzl b/internal/common/json_marshal.bzl new file mode 100644 index 00000000..cb2f58cd --- /dev/null +++ b/internal/common/json_marshal.bzl @@ -0,0 +1,35 @@ +# 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. + +"""Marshal an arbitrary Skylark object to JSON.""" +def json_marshal(data): + """Serializes arbitrary data to JSON. + + Args: + data: any object + + Returns: + JSON string representing the data + """ + if type(data) == "dict" or type(data) == "list": + return str(data).replace(": True", ": true").replace(": False", ": false").replace(": None", ": false") + elif type(data) == "int": + return str(data) + elif type(data) == "string": + return "\"" + data + "\"" + elif type(data) == "Label": + return "\"//{}:{}\"".format(data.package, data.name) + elif type(data) == "bool": + return "true" if data else "false" + return "unknown type {}: {}".format(type(data), data) diff --git a/internal/common/tsconfig.bzl b/internal/common/tsconfig.bzl new file mode 100644 index 00000000..956bac0e --- /dev/null +++ b/internal/common/tsconfig.bzl @@ -0,0 +1,143 @@ +# 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. + +"""Helpers for configuring the TypeScript compiler. +""" +_DEBUG = False + +def create_tsconfig(ctx, files, srcs, tsconfig_path, + devmode_manifest=None, tsickle_externs=None, type_blacklisted_declarations=[], + out_dir=None, disable_strict_deps=False, allowed_deps=set(), + extra_root_dirs=[]): + """Creates an object representing the TypeScript configuration + to run the compiler under Bazel. + + Args: + ctx: the skylark execution context + files: Labels of all TypeScript compiler inputs + srcs: Immediate sources being compiled, as opposed to transitive deps. + tsconfig_path: where the resulting config will be written; paths will be relative + to this folder + devmode_manifest: path to the manifest file to write for --target=es5 + tsickle_externs: path to write tsickle-generated externs.js. + type_blacklisted_declarations: types declared in these files will never be + mentioned in generated .d.ts. + out_dir: directory for generated output. Default is ctx.bin_dir + disable_strict_deps: whether to disable the strict deps check + allowed_deps: the set of files that code in srcs may depend on (strict deps) + extra_root_dirs: Extra root dirs to be passed to tsc_wrapped. + """ + outdir_path = out_dir if out_dir != None else ctx.configuration.bin_dir.path + workspace_path = "/".join([".."] * len(tsconfig_path.split("/"))) + + perf_trace_path = "/".join([ctx.configuration.bin_dir.path, ctx.label.package, + ctx.label.name + ".trace"]) + # TODO(alexeagle): a better way to ask for the perf trace than editing here? + perf_trace_path = "" # Comment out => receive perf trace! + + # Options for running the TypeScript compiler under Bazel. + # See javascript/typescript/compiler/tsc_wrapped.ts:BazelOptions. + # Unlike compiler_options, the paths here are relative to the rootDir, + # not the location of the tsconfig.json file. + bazel_options = { + "target": ctx.label, + "tsickle": tsickle_externs != None, + "tsickleGenerateExterns": getattr(ctx.attr, "generate_externs", True), + "tsickleExternsPath": tsickle_externs.path if tsickle_externs else "", + "untyped": not getattr(ctx.attr, "tsickle_typed", False), + "typeBlackListPaths": [f.path for f in type_blacklisted_declarations], + + # Substitute commonjs with googmodule. + "googmodule": ctx.attr.runtime == "browser", + "es5Mode": devmode_manifest != None, + "manifest": devmode_manifest.path if devmode_manifest else "", + # Explicitly tell the compiler which sources we're interested in (emitting + # and type checking). + "compilationTargetSrc": [s.path for s in srcs], + "disableStrictDeps": disable_strict_deps, + "allowedStrictDeps": [f.path for f in allowed_deps], + "perfTracePath": perf_trace_path, + "enableConformance": getattr(ctx.attr, "enable_conformance", False), + } + + # Keep these options in sync with those in playground/playground.ts. + compiler_options = { + # De-sugar to this language level + "target": "es5" if devmode_manifest or ctx.attr.runtime == "nodejs" else "es6", + "downlevelIteration": devmode_manifest != None or ctx.attr.runtime == "nodejs", + + # Do not type-check the lib.*.d.ts. + # We think this shouldn't be necessary but haven't figured out why yet + # and builds are faster with the setting on. + # http://b/30709121 + "skipDefaultLibCheck": True, + + # Always produce commonjs modules (might get translated to goog.module). + "module": "commonjs", + "moduleResolution": "node", + + "outDir": "/".join([workspace_path, outdir_path]), + + # We must set a rootDir to avoid TypeScript emit paths varying + # due computeCommonSourceDirectory behavior. + # TypeScript requires the rootDir be a parent of all sources in + # files[], so it must be set to the workspace_path. + "rootDir": workspace_path, + + # Path handling for resolving modules, see specification at + # https://github.com/Microsoft/TypeScript/issues/5039 + # Paths where we attempt to load relative references. + # Longest match wins + # + # tsc_wrapped also uses this property to strip leading paths + # to produce a flattened output tree, see + # https://github.com/Microsoft/TypeScript/issues/8245 + "rootDirs": ["/".join([workspace_path, e]) for e in extra_root_dirs] + [ + workspace_path, + "/".join([workspace_path, ctx.configuration.genfiles_dir.path]), + "/".join([workspace_path, ctx.configuration.bin_dir.path]), + ], + + "traceResolution": _DEBUG, + "diagnostics": _DEBUG, + + # Inline const enums. + "preserveConstEnums": False, + + # permit `@Decorator` syntax and allow runtime reflection on their types. + "experimentalDecorators": True, + "emitDecoratorMetadata": True, + + # Interpret JSX as React calls (until someone asks for something different) + "jsx": "react", + "jsxFactory": "React.createElement", + + "noEmitOnError": False, + "declaration": True, + "stripInternal": True, + + # Embed source maps and sources in .js outputs + "inlineSourceMap": True, + "inlineSources": True, + + # Don't emit decorate/metadata helper code, we provide our own helpers.js. + "noEmitHelpers": ctx.attr.runtime == "browser", + } + + return { + "compilerOptions": compiler_options, + "bazelOptions": bazel_options, + "files": [workspace_path + "/" + f.path for f in files], + "compileOnSave": False + } diff --git a/internal/executables.bzl b/internal/executables.bzl new file mode 100644 index 00000000..95844d3e --- /dev/null +++ b/internal/executables.bzl @@ -0,0 +1,22 @@ +# 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. + +"""node and TypeScript compiler labels. +""" + +def get_tsc(): + return Label("//internal:tsc_wrapped") + +def get_node(): + return Label("@io_bazel_rules_typescript_node//:bin/node") diff --git a/internal/node.bzl b/internal/node.bzl new file mode 100644 index 00000000..3489539f --- /dev/null +++ b/internal/node.bzl @@ -0,0 +1,61 @@ +# 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. + +"""Rules for executing programs in the nodejs runtime. +""" +load(":executables.bzl", "get_node") + +def _node_binary_impl(ctx): + node = ctx.file._node + script = ctx.attr.main + node_modules = ctx.files._node_modules + + ctx.template_action( + template=ctx.file._launcher_template, + output=ctx.outputs.executable, + substitutions={ + "TEMPLATED_node": ctx.workspace_name + "/" + node.path, + "TEMPLATED_args": " ".join(ctx.attr.args), + "TEMPLATED_script_path": script, + }, + executable=True, + ) + + return struct( + runfiles = ctx.runfiles( + files = [node] + node_modules, + collect_data = True, + ), + ) + +node_binary = rule( + _node_binary_impl, + attrs = { + "main": attr.string(), + "data": attr.label_list( + allow_files = True, + cfg = "data"), + "_node": attr.label( + default = get_node(), + allow_files = True, + single_file = True), + "_node_modules": attr.label( + default = Label("@yarn//installed:node_modules")), + "_launcher_template": attr.label( + default = Label("//internal:node_launcher.sh"), + allow_files = True, + single_file = True) + }, + executable = True, +) diff --git a/internal/node_install.bzl b/internal/node_install.bzl new file mode 100644 index 00000000..beb6aee2 --- /dev/null +++ b/internal/node_install.bzl @@ -0,0 +1,70 @@ +# 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. + +"""Install NodeJS when the user runs node_repositories() from their WORKSPACE. + +We fetch a specific version of Node, to ensure builds are hermetic. +We then create a repository @io_bazel_rules_typescript_node which provides the +node binary to other rules. +""" + +def _node_impl(repository_ctx): + repository_ctx.file("BUILD", content=""" +package(default_visibility = ["//visibility:public"]) +exports_files([ + "bin/node", + "bin/npm", +]) +""") + repository_ctx.file("BUILD.bazel", content=""" +package(default_visibility = ["//visibility:public"]) +exports_files([ + "bin/node", + "bin/npm", +]) +""") + + os_name = repository_ctx.os.name.lower() + if os_name.startswith("mac os"): + repository_ctx.download_and_extract( + [ + "http://mirror.bazel.build/nodejs.org/dist/v6.10.2/node-v6.10.2-darwin-x64.tar.xz", + "https://nodejs.org/dist/v6.10.2/node-v6.10.2-darwin-x64.tar.xz", + ], + stripPrefix = "node-v6.10.2-darwin-x64", + sha256 = "360b887361b2597613f18968e3fc0e920079a363d0535fc4e40532e3426fc6eb" + ) + elif os_name.find("windows") != -1: + repository_ctx.download_and_extract( + [ + "http://mirror.bazel.build/nodejs.org/dist/v6.10.2/node-v6.10.2-win-x64.zip", + "http://nodejs.org/dist/v6.10.2/node-v6.10.2-win-x64.zip", + ], + stripPrefix = "node-v6.10.2-win-x64", + sha256 = "d778ed84685c6604192cfcf40192004e27fb11c9e65c3ce4b283d90703b4192c" + ) + else: + repository_ctx.download_and_extract( + [ + "http://mirror.bazel.build/nodejs.org/dist/v6.10.2/node-v6.10.2-linux-x64.tar.xz", + "http://nodejs.org/dist/v6.10.2/node-v6.10.2-linux-x64.tar.xz", + ], + stripPrefix = "node-v6.10.2-linux-x64", + sha256 = "b519cd616b0671ab789d2645c5c026deb7e016d73a867ab4b1b8c9ceba9c3503" + ) + +_node_repo = repository_rule(_node_impl, attrs = {}) + +def node_repositories(): + _node_repo(name = "io_bazel_rules_typescript_node") diff --git a/internal/node_launcher.sh b/internal/node_launcher.sh new file mode 100644 index 00000000..30927360 --- /dev/null +++ b/internal/node_launcher.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# 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. + +set -e + +# Launcher for NodeJS applications. +# Find our runfiles tree. We need this to launch node with the correct +# entry point. +# +# Call this program X. X was generated by a genrule and may be invoked +# in many ways: +# 1a) directly by a user, with $0 in the output tree +# 1b) via 'bazel run' (similar to case 1a) +# 2) directly by a user, with $0 in X's runfiles tree +# 3) by another program Y which has a data dependency on X, with $0 in Y's +# runfiles tree +# 4a) via 'bazel test' +# 4b) case 3 in the context of a test +# 5a) by a genrule cmd, with $0 in the output tree +# 6a) case 3 in the context of a genrule +# +# For case 1, $0 will be a regular file, and the runfiles tree will be +# at $0.runfiles. +# For case 2 or 3, $0 will be a symlink to the file seen in case 1. +# For case 4, $TEST_SRCDIR should already be set to the runfiles tree by +# blaze. +# Case 5a is handled like case 1. +# Case 6a is handled like case 3. + +case "$0" in + /*) self="$0" ;; + *) self="$PWD/$0" ;; +esac + +if [[ -n "$TEST_SRCDIR" ]]; then + # Case 4, bazel has identified runfiles for us. + RUNFILES="$TEST_SRCDIR" +else + while true; do + if [[ -e "$self.runfiles" ]]; then + RUNFILES="$self.runfiles" + break + fi + + if [[ $self == *.runfiles/* ]]; then + RUNFILES="${self%%.runfiles/*}.runfiles" + # don't break; this is a last resort for case 6b + fi + + if [[ ! -L "$self" ]]; then + break; + fi + + readlink="$(readlink "$self")" + if [[ "$readlink" = /* ]]; then + self="$readlink" + else + # resolve relative symlink + self="${self%%/*}/$readlink" + fi + done + + if [[ -z "$RUNFILES" ]]; then + echo " >>>> FAIL: RUNFILES environment variable is not set. <<<<" >&2 + exit 1 + fi +fi +export RUNFILES +export NODE_PATH="${RUNFILES}" + +ARGS=() +NODE_OPTIONS=() +ALL_ARGS=(TEMPLATED_args $@) +for ARG in "${ALL_ARGS[@]}"; do + case "$ARG" in + --node_options=*) NODE_OPTIONS+=( "${ARG#--node_options=}" ) ;; + *) ARGS+=( "$ARG" ) + esac +done + +exec "${RUNFILES}/TEMPLATED_node" "${NODE_OPTIONS[@]}" "${RUNFILES}/TEMPLATED_script_path" "${ARGS[@]}" diff --git a/internal/tsc_wrapped/BUILD b/internal/tsc_wrapped/BUILD new file mode 100644 index 00000000..46754d28 --- /dev/null +++ b/internal/tsc_wrapped/BUILD @@ -0,0 +1,7 @@ +load("//:defs.bzl", "ts_library") + +ts_library( + name = "tsc_wrapped", + srcs = glob(["*.ts"]), + tsconfig = ":tsconfig.json", +) diff --git a/internal/tsc_wrapped/tsconfig.json b/internal/tsc_wrapped/tsconfig.json new file mode 100644 index 00000000..80c821ca --- /dev/null +++ b/internal/tsc_wrapped/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": [ + "node" + ] + } +} diff --git a/internal/tsc_wrapped/tsconfig.ts b/internal/tsc_wrapped/tsconfig.ts new file mode 100644 index 00000000..a2dda5fb --- /dev/null +++ b/internal/tsc_wrapped/tsconfig.ts @@ -0,0 +1,131 @@ +import * as path from 'path'; // from //third_party/javascript/typings/node +import * as ts from 'typescript'; + +/** + * The configuration block provided by the tsconfig "bazelOptions". + * Note that all paths here are relative to the rootDir, not absolute nor + * relative to the location containing the tsconfig file. + */ +export interface BazelOptions { + /** The full bazel target that is being built, e.g. //my/pkg:library. */ + target: string; + + /** If true, convert require()s into goog.module(). */ + googmodule: boolean; + + /** If true, emit ES5 into filename.es5.js. */ + es5Mode: boolean; + + /** If true, convert TypeScript code into a Closure-compatible variant. */ + tsickle: boolean; + + /** If true, generate externs from declarations in d.ts files. */ + tsickleGenerateExterns: boolean; + + /** Write generated externs to the given path. */ + tsickleExternsPath: string; + + /** Paths of declarations whose types must not appear in result .d.ts. */ + typeBlackListPaths: string[]; + + /** If true, emit Closure types in TypeScript->JS output. */ + untyped: boolean; + + /** The list of sources we're interested in (emitting and type checking). */ + compilationTargetSrc: string[]; + + /** Path to write the module dependency manifest to. */ + manifest: string; + + /** + * Whether to disable strict deps check. If true the next parameter is + * ignored. + */ + disableStrictDeps?: boolean; + + /** + * Paths of dependencies that are allowed by strict deps, i.e. that may be + * imported by the source files in compilationTargetSrc. + */ + allowedStrictDeps: string[]; + + /** Write a performance trace to this path. Disabled when falsy. */ + perfTracePath?: string; + + /** If true, enable the conformance check plugin in TSC. */ + enableConformance: boolean; + + /** + * An additional prelude to insert after the `goog.module` call, + * e.g. with additional imports or requires. + */ + prelude: string; + + /** + * Name of the current locale if processing a locale-specific file. + */ + locale?: string; + + /** + * A list of errors this compilation is expected to generate, in the form + * "TS1234:regexp". If empty, compilation is expected to succeed. + */ + expectedDiagnostics: string[]; +} + +export interface ParsedTsConfig { + options: ts.CompilerOptions; + bazelOpts: BazelOptions; + files: string[]; +} + +/** + * Load a tsconfig.json and convert all referenced paths (including + * bazelOptions) to absolute paths. + * Paths seen by TypeScript should be absolute, to match behavior + * of the tsc ModuleResolution implementation. + * @param tsconfigFile path to tsconfig, relative to process.cwd() or absolute + * @return configuration parsed from the file, or error diagnostics + */ +export function parseTsconfig( + tsconfigFile: string, host: ts.ParseConfigHost = ts.sys): + [ParsedTsConfig, ts.Diagnostic[], {target: string}] { + // TypeScript expects an absolute path for the tsconfig.json file + tsconfigFile = path.resolve(tsconfigFile); + + const {config, error} = ts.readConfigFile(tsconfigFile, host.readFile); + if (error) { + // target is in the config file we failed to load... + return [null, [error], {target: ''}]; + } + + const bazelOpts: BazelOptions = config.bazelOptions; + const target = bazelOpts.target; + const {options, errors} = + // All paths in the compilerOptions will be converted to absolute. + ts.convertCompilerOptionsFromJson( + config.compilerOptions, path.dirname(tsconfigFile)); + if (errors && errors.length) { + return [null, errors, {target}]; + } + + // The file list in the tsconfig is relative to the tsconfig dir + // and aren't transformed by convertCompilerOptionsFromJson. + // Transform them to be absolute here. + const files = config.files.map(f => { + return path.resolve(path.dirname(tsconfigFile), f); + }); + + // The bazelOpts paths in the tsconfig are relative to + // options.rootDir (the google3 root) and aren't transformed by + // convertCompilerOptionsFromJson (because TypeScript doesn't know + // about them). Transform them to also be absolute here. + bazelOpts.compilationTargetSrc = + bazelOpts.compilationTargetSrc.map(f => path.resolve(options.rootDir, f)); + bazelOpts.allowedStrictDeps = + bazelOpts.allowedStrictDeps.map(f => path.resolve(options.rootDir, f)); + bazelOpts.typeBlackListPaths = + bazelOpts.typeBlackListPaths.map(f => path.resolve(options.rootDir, f)); + + return [{options, bazelOpts, files}, null, {target}]; +} diff --git a/internal/yarn_install.bzl b/internal/yarn_install.bzl new file mode 100644 index 00000000..ad886d54 --- /dev/null +++ b/internal/yarn_install.bzl @@ -0,0 +1,92 @@ +# 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. + +"""Install Yarn and run `yarn install` when the user calls yarn_install() from their WORKSPACE. + +Yarn is a package manager that downloads dependencies. Yarn is an improvement over the `npm` tool in +speed and correctness. + +We download a specific version of Yarn to ensure a hermetic build. +Then, using the package.json file supplied by the user, we call `yarn install` +to create or update a node_modules folder next to the package.json. +Finally we create a workspace that symlinks to the user's project. +We name this workspace "yarn" so there will be targets like +@yarn//installed:node_modules + +Within the user's project, they can refer to //:node_modules +but from other repositories, like the @io_bazel_rules_typescript +repository, we also need to find some labels under node_modules. +""" + +load(":executables.bzl", "get_node") + +def _yarn_install_impl(ctx): + project_dir = ctx.path(ctx.attr.package_json).dirname + ctx.file("yarn_install.sh", """#!/bin/bash +set -ex +ROOT=$(dirname $1) +NODE=$2 +YARN=$3 +(cd $ROOT; $NODE $YARN install) +""") + result = ctx.execute(["./yarn_install.sh", + ctx.path(ctx.attr.package_json), + ctx.path(ctx.attr._node), + ctx.path(ctx.attr._yarn)]) + if result.return_code > 0: + print(result.stdout) + print(result.stderr) + + # WORKAROUND for https://github.com/bazelbuild/bazel/issues/374#issuecomment-296217940 + # Bazel does not allow labels to start with `@`, so when installing eg. the `@types/node` + # module from the @types scoped package, you'll get an error. + # The workaround is to move the rule up one level, from /node_modules to the project root. + # For now, users must instead write their own /BUILD file on setup. + + # ctx.symlink(project_dir.get_child("node_modules"), "node_modules") + # add a BUILD file inside the user's node_modules project folder + # ctx.file("installed/BUILD", """ + # filegroup(name = "node_modules", srcs = glob(["node_modules/**/*"]), visibility = ["//visibility:public"]) + # """) + + # Instead symlink the root directory from the user's workspace + ctx.symlink(project_dir, "installed") + + +_yarn_install = repository_rule( + _yarn_install_impl, + attrs = { + "package_json": attr.label(), + "_node": attr.label(default = get_node(), allow_files=True, single_file=True), + "_yarn": attr.label(default = Label("@yarn_pkg//:bin/yarn.js")), + }, +) + +def yarn_install(package_json): + native.new_http_archive( + name = "yarn_pkg", + urls = [ + "http://mirror.bazel.build/github.com/yarnpkg/yarn/releases/download/v0.22.0/yarn-v0.22.0.tar.gz", + "https://github.com/yarnpkg/yarn/releases/download/v0.22.0/yarn-v0.22.0.tar.gz", + ], + strip_prefix = "dist", + type = "tar.gz", + build_file_content = """ +package(default_visibility = ["//visibility:public"]) +exports_files(["bin/yarn"]) +""", + ) + + _yarn_install(name = "yarn", package_json = package_json) + diff --git a/package.json b/package.json new file mode 100644 index 00000000..e5f6c79b --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "bazel-ts", + "description": "Build TypeScript with Bazel", + "version": "0.0.0", + "dependencies": { + "typescript": "2.3" + }, + "devDependencies": { + "@types/node": "^7.0.18", + "clang-format": "1.0.49" + } +}