Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typescript): generate tsconfig.json for ts_project #2130

Merged
merged 4 commits into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 11 additions & 26 deletions packages/typescript/checked_in_ts_project.bzl
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"checked_in_ts_project rule"

load("@build_bazel_rules_nodejs//:index.bzl", "generated_file_test")
load("@build_bazel_rules_nodejs//third_party/github.com/bazelbuild/bazel-skylib:rules/write_file.bzl", "write_file")
load("//packages/typescript:index.bzl", "ts_project")

def checked_in_ts_project(name, src, checked_in_js = None, **kwargs):
Expand All @@ -15,34 +14,20 @@ def checked_in_ts_project(name, src, checked_in_js = None, **kwargs):
if not checked_in_js:
checked_in_js = src[:-3] + ".js"

tsconfig = "tsconfig_%s.json" % name

# workspace is up three dirs (bazel-out/arch/bin) plus number of segments in the package
workspace_root = "/".join([".."] * (3 + len(native.package_name().split("/"))))

# Generate a tsconfig, this is partly an example of how it can be done, per jbedard and toxicable request
write_file(
name = "_gen_tsconfig_%s" % name,
content = [struct(
compilerOptions = struct(
lib = ["es2017", "dom"],
strict = True,
target = "es2015",
module = "commonjs",
removeComments = True,
declaration = True,
skipLibCheck = True,
),
files = ["/".join([workspace_root, native.package_name(), src])],
).to_json()],
out = tsconfig,
)

ts_project(
name = name,
srcs = [src],
declaration = True,
tsconfig = tsconfig,
tsconfig = {
"compilerOptions": {
"declaration": True,
"lib": ["es2017", "dom"],
"module": "commonjs",
"removeComments": True,
"skipLibCheck": True,
"strict": True,
"target": "es2015",
},
},
**kwargs
)

Expand Down
76 changes: 76 additions & 0 deletions packages/typescript/internal/ts_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,79 @@ feature from TypeScript, then the Bazel implementation needs to know about that
extended configuration file as well, to pass them both to the TypeScript compiler.
""",
)

def _join(*elements):
return "/".join([f for f in elements if f])

def _relative_path(tsconfig, dest):
relative_to = tsconfig.dirname
if dest.is_source:
# Calculate a relative path from the directory where we're writing the tsconfig
# back to the sources root
workspace_root = "/".join([".."] * len(relative_to.split("/")))
return _join(workspace_root, dest.path)

# Bazel guarantees that srcs are beneath the package directory, and we disallow
# tsconfig.json being generated with a "/" in the name.
# So we can calculate a relative path from e.g.
# bazel-out/darwin-fastbuild/bin/packages/typescript/test/ts_project/generated_tsconfig/gen_src
# to <generated file packages/typescript/test/ts_project/generated_tsconfig/gen_src/subdir/a.ts>
return dest.path[len(relative_to) + 1:]

def _write_tsconfig_rule(ctx):
# TODO: is it useful to expand Make variables in the content?
content = "\n".join(ctx.attr.content)
if ctx.attr.extends:
content = content.replace(
"__extends__",
_relative_path(ctx.outputs.out, ctx.file.extends),
)
if ctx.attr.files:
content = content.replace(
"\"__files__\"",
str([_relative_path(ctx.outputs.out, f) for f in ctx.files.files]),
)
ctx.actions.write(
output = ctx.outputs.out,
content = content,
)
return [DefaultInfo(files = depset([ctx.outputs.out]))]

write_tsconfig_rule = rule(
implementation = _write_tsconfig_rule,
attrs = {
"content": attr.string_list(),
"extends": attr.label(allow_single_file = True),
"files": attr.label_list(allow_files = True),
"out": attr.output(),
},
)

# Syntax sugar around skylib's write_file
def write_tsconfig(name, config, files, out, extends = None):
"""Wrapper around bazel_skylib's write_file which understands tsconfig paths

Args:
name: name of the resulting write_file rule
config: tsconfig dictionary
files: list of input .ts files to put in the files[] array
out: the file to write
extends: a label for a tsconfig.json file to extend from, if any
"""
if out.find("/") >= 0:
fail("tsconfig should be generated in the package directory, to make relative pathing simple")

if extends:
config["extends"] = "__extends__"

amended_config = struct(
files = "__files__",
**config
)
write_tsconfig_rule(
name = name,
files = files,
extends = extends,
content = [amended_config.to_json()],
out = out,
)
152 changes: 124 additions & 28 deletions packages/typescript/internal/ts_project.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "NpmPackageInfo", "declaration_info", "js_module_info", "run_node")
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "module_mappings_aspect")
load(":ts_config.bzl", "TsConfigInfo")
load(":ts_config.bzl", "TsConfigInfo", "write_tsconfig")

_DEFAULT_TSC = (
# BEGIN-INTERNAL
Expand Down Expand Up @@ -39,11 +39,22 @@ _OUTPUTS = {
}

def _join(*elements):
return "/".join([f for f in elements if f])
segments = [f for f in elements if f]
if len(segments):
return "/".join(segments)
return "."

def _ts_project_impl(ctx):
arguments = ctx.actions.args()

generated_srcs = False
for src in ctx.files.srcs:
if src.is_source:
if generated_srcs:
fail("srcs cannot be a mix of generated files and source files")
else:
generated_srcs = True

# Add user specified arguments *before* rule supplied arguments
arguments.add_all(ctx.attr.args)

Expand All @@ -53,7 +64,11 @@ def _ts_project_impl(ctx):
"--outDir",
_join(ctx.bin_dir.path, ctx.label.package, ctx.attr.out_dir),
"--rootDir",
_join(ctx.label.package, ctx.attr.root_dir) if ctx.label.package else ".",
_join(
ctx.bin_dir.path if generated_srcs else None,
ctx.label.package,
ctx.attr.root_dir,
),
])
if len(ctx.outputs.typings_outs) > 0:
declaration_dir = ctx.attr.declaration_dir if ctx.attr.declaration_dir else ctx.attr.out_dir
Expand Down Expand Up @@ -90,12 +105,17 @@ def _ts_project_impl(ctx):
deps_depsets.append(dep[DeclarationInfo].transitive_declarations)

inputs = ctx.files.srcs + depset(transitive = deps_depsets).to_list()

# Gather TsConfig info from both the direct (tsconfig) and indirect (extends) attribute
if TsConfigInfo in ctx.attr.tsconfig:
inputs.extend(ctx.attr.tsconfig[TsConfigInfo].deps)
else:
inputs.append(ctx.file.tsconfig)
if ctx.attr.extends:
inputs.extend(ctx.files.extends)
for extend in ctx.attr.extends:
if TsConfigInfo in extend:
inputs.extend(extend[TsConfigInfo].deps)
else:
inputs.extend(extend.files.to_list())

# We do not try to predeclare json_outs, because their output locations generally conflict with their path in the source tree.
# (The exception is when out_dir is used, then the .json output is a different path than the input.)
Expand Down Expand Up @@ -359,14 +379,51 @@ def ts_project_macro(

deps: List of labels of other rules that produce TypeScript typings (.d.ts files)

tsconfig: Label of the tsconfig.json file to use for the compilation, or a target that provides TsConfigInfo.
tsconfig: Label of the tsconfig.json file to use for the compilation

By default, we assume the tsconfig file is named by adding `.json` to the `name` attribute.
To support "chaining" of more than one extended config, this label could be a target that
provdes `TsConfigInfo` such as `ts_config`.

extends: List of labels of tsconfig file(s) referenced in `extends` section of tsconfig.
By default, we assume the tsconfig file is named by adding `.json` to the `name` attribute.

Any tsconfig files "chained" by extends clauses must either be transitive deps of the TsConfigInfo
provided to the `tsconfig` attribute, or must be explicitly listed here.
EXPERIMENTAL: generated tsconfig

Instead of a label, you can pass a dictionary of tsconfig keys.

In this case, a tsconfig.json file will be generated for this compilation, in the following way:
- all top-level keys will be copied by converting the dict to json.
So `tsconfig = {"compilerOptions": {"declaration": True}}`
will result in a generated `tsconfig.json` with `{"compilerOptions": {"declaration": true}}`
- each file in srcs will be converted to a relative path in the `files` section.
- the `extends` attribute will be converted to a relative path

Note that you can mix and match attributes and compilerOptions properties, so these are equivalent:

```
ts_project(
tsconfig = {
"compilerOptions": {
"declaration": True,
},
},
)
```
and
```
ts_project(
declaration = True,
)
```

extends: Label of the tsconfig file referenced in the `extends` section of tsconfig

To support "chaining" of more than one extended config, this label could be a target that
provdes `TsConfigInfo` such as `ts_config`.

_DEPRECATED, to be removed in 3.0_:
For backwards compatibility, this accepts a list of Labels of the "chained"
tsconfig files. You should instead use a single Label of a `ts_config` target.
Follow this deprecation: https://github.com/bazelbuild/rules_nodejs/issues/2140

args: List of strings of additional command-line arguments to pass to tsc.

Expand Down Expand Up @@ -413,31 +470,70 @@ def ts_project_macro(

if srcs == None:
srcs = native.glob(["**/*.ts", "**/*.tsx"])

if tsconfig == None:
tsconfig = name + ".json"

extra_deps = []

if validate:
validate_options(
name = "_validate_%s_options" % name,
target = "//%s:%s" % (native.package_name(), name),
declaration = declaration,
source_map = source_map,
declaration_map = declaration_map,
composite = composite,
incremental = incremental,
emit_declaration_only = emit_declaration_only,
ts_build_info_file = ts_build_info_file,
tsconfig = tsconfig,
extends = extends,
if type(tsconfig) == type(dict()):
# Opt-in to #2140 breaking change at the same time you opt-in to experimental tsconfig dict
if type(extends) == type([]):
fail("when tsconfig is a dict, extends should have a single value")

# Copy attributes <-> tsconfig properties
# TODO: fail if compilerOptions includes a conflict with an attribute?
compiler_options = tsconfig.setdefault("compilerOptions", {})
source_map = compiler_options.setdefault("sourceMap", source_map)
declaration = compiler_options.setdefault("declaration", declaration)
declaration_map = compiler_options.setdefault("declarationMap", declaration_map)
emit_declaration_only = compiler_options.setdefault("emitDeclarationOnly", emit_declaration_only)

# These options are always passed on the tsc command line so don't include them
# in the tsconfig. At best they're redundant, but at worst we'll have a conflict
if "outDir" in compiler_options.keys():
out_dir = compiler_options.pop("outDir")
if "declarationDir" in compiler_options.keys():
declaration_dir = compiler_options.pop("declarationDir")
if "rootDir" in compiler_options.keys():
root_dir = compiler_options.pop("rootDir")

# FIXME: need to remove keys that have a None value?
write_tsconfig(
name = "_gen_tsconfig_%s" % name,
config = tsconfig,
files = srcs,
extends = Label("//%s:%s" % (native.package_name(), name)).relative(extends) if extends else None,
out = "tsconfig_%s.json" % name,
)
extra_deps.append("_validate_%s_options" % name)

# From here, tsconfig becomes a file, the same as if the
# user supplied a tsconfig.json InputArtifact
tsconfig = "tsconfig_%s.json" % name

else:
if tsconfig == None:
tsconfig = name + ".json"

if validate:
validate_options(
name = "_validate_%s_options" % name,
target = "//%s:%s" % (native.package_name(), name),
declaration = declaration,
source_map = source_map,
declaration_map = declaration_map,
composite = composite,
incremental = incremental,
ts_build_info_file = ts_build_info_file,
emit_declaration_only = emit_declaration_only,
tsconfig = tsconfig,
extends = extends,
)
extra_deps.append("_validate_%s_options" % name)

typings_out_dir = declaration_dir if declaration_dir else out_dir
tsbuildinfo_path = ts_build_info_file if ts_build_info_file else name + ".tsbuildinfo"

# Backcompat for extends as a list, to cleanup in #2140
if (type(extends) == type("")):
extends = [extends]

ts_project(
name = name,
srcs = srcs,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"Test that properties in the tsconfig dict are honored"

load("@build_bazel_rules_nodejs//:index.bzl", "generated_file_test")
load("//packages/typescript:index.bzl", "ts_project")

ts_project(
tsconfig = {
"compilerOptions": {
"declaration": True,
"declarationDir": "types",
"declarationMap": True,
"module": "esnext",
"outDir": "out",
"rootDir": "src",
"sourceMap": True,
"types": [],
},
},
)

generated_file_test(
name = "test",
src = "expected.js_",
generated = ":out/a.js",
)

generated_file_test(
name = "test_map",
src = "expected.js.map_",
generated = ":out/a.js.map",
)

generated_file_test(
name = "test_dts",
src = "expected.d.ts_",
generated = ":types/a.d.ts",
)

generated_file_test(
name = "test_dtsmap",
src = "expected.d.ts.map_",
generated = ":types/a.d.ts.map",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/typescript/test/ts_project/generated_tsconfig/config/src/a.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,CAAC,EAAE,MAAsB,CAAC"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export declare const a: string;
//# sourceMappingURL=a.d.ts.map
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":3,"file":"a.js","sourceRoot":"","sources":["../../../../../../../../../../packages/typescript/test/ts_project/generated_tsconfig/config/src/a.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,IAAM,CAAC,GAAW,aAAa,CAAC"}
Loading