Skip to content

Commit

Permalink
feat(typescript): added support for using non-file targets in srcs of…
Browse files Browse the repository at this point in the history
… ts_project
  • Loading branch information
twheys authored and alexeagle committed Feb 2, 2022
1 parent 7e627f7 commit 96d37b6
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 66 deletions.
52 changes: 14 additions & 38 deletions packages/typescript/index.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def ts_project(
write_tsconfig(
name = "_gen_tsconfig_%s" % name,
config = tsconfig,
files = [s for s in srcs if _lib.is_ts_src(s, allow_js) or _lib.is_json_src(s, resolve_json_module)],
files = srcs,
extends = Label("%s//%s:%s" % (native.repository_name(), native.package_name(), name)).relative(extends) if extends else None,
out = "tsconfig_%s.json" % name,
)
Expand Down Expand Up @@ -467,28 +467,11 @@ def ts_project(
tsc = ":" + tsc_worker
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"
js_outs = []
map_outs = []
typings_outs = []
typing_maps_outs = []

if not emit_declaration_only:
exts = {
"*": ".js",
".jsx": ".jsx",
".tsx": ".jsx",
} if preserve_jsx else {"*": ".js"}
js_outs.extend(_lib.out_paths(srcs, out_dir, root_dir, allow_js, exts))
if source_map and not emit_declaration_only:
exts = {
"*": ".js.map",
".tsx": ".jsx.map",
} if preserve_jsx else {"*": ".js.map"}
map_outs.extend(_lib.out_paths(srcs, out_dir, root_dir, False, exts))
if declaration or composite:
typings_outs.extend(_lib.out_paths(srcs, typings_out_dir, root_dir, allow_js, {"*": ".d.ts"}))
if declaration_map:
typing_maps_outs.extend(_lib.out_paths(srcs, typings_out_dir, root_dir, allow_js, {"*": ".d.ts.map"}))

js_outs = _lib.calculate_js_outs(srcs, out_dir, root_dir, allow_js, preserve_jsx, emit_declaration_only)
map_outs = _lib.calculate_map_outs(srcs, out_dir, root_dir, source_map, preserve_jsx, emit_declaration_only)
typings_outs = _lib.calculate_typings_outs(srcs, typings_out_dir, root_dir, declaration, composite, allow_js)
typing_maps_outs = _lib.calculate_typing_maps_outs(srcs, typings_out_dir, root_dir, declaration_map, allow_js)

tsc_js_outs = []
tsc_map_outs = []
Expand Down Expand Up @@ -557,36 +540,29 @@ def ts_project(
**common_kwargs
)

if not len(tsc_js_outs) and not len(typings_outs):
label = "//{}:{}".format(native.package_name(), name)
if transpiler:
no_outs_msg = "ts_project target %s with custom transpiler needs `declaration = True`." % label
else:
no_outs_msg = """ts_project target %s is configured to produce no outputs.
This might be because
- you configured it with `noEmit`
- the `srcs` are empty
""" % label
fail(no_outs_msg + """
This is an error because Bazel does not run actions unless their outputs are needed for the requested targets to build.
""")

_ts_project(
name = tsc_target_name,
srcs = srcs,
args = args,
deps = tsc_deps,
tsconfig = tsconfig,
allow_js = allow_js,
extends = extends,
incremental = incremental,
preserve_jsx = preserve_jsx,
composite = composite,
declaration = declaration,
declaration_dir = declaration_dir,
source_map = source_map,
declaration_map = declaration_map,
out_dir = out_dir,
root_dir = root_dir,
js_outs = tsc_js_outs,
map_outs = tsc_map_outs,
typings_outs = typings_outs,
typing_maps_outs = typing_maps_outs,
buildinfo_out = tsbuildinfo_path if composite or incremental else None,
emit_declaration_only = emit_declaration_only,
tsc = tsc,
link_workspace_root = link_workspace_root,
supports_workers = supports_workers,
Expand Down
102 changes: 90 additions & 12 deletions packages/typescript/internal/ts_project.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ _DEFAULT_TSC = (
"//typescript/bin:tsc"
)

_ATTRS = {
_ATTRS = dict(_validate_lib.attrs, **{
"args": attr.string_list(),
"data": attr.label_list(default = [], allow_files = True),
"declaration_dir": attr.string(),
Expand All @@ -25,7 +25,6 @@ _ATTRS = {
],
aspects = [module_mappings_aspect],
),
"extends": attr.label(allow_files = [".json"]),
"link_workspace_root": attr.bool(),
"out_dir": attr.string(),
"root_dir": attr.string(),
Expand All @@ -36,9 +35,9 @@ _ATTRS = {
"srcs": attr.label_list(allow_files = True, mandatory = True),
"supports_workers": attr.bool(default = False),
"tsc": attr.label(default = Label(_DEFAULT_TSC), executable = True, cfg = "exec"),
"transpile": attr.bool(doc = "whether tsc should be used to produce .js outputs"),
"transpile": attr.bool(doc = "whether tsc should be used to produce .js outputs", default = True),
"tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]),
}
})

# tsc knows how to produce the following kinds of output files.
# NB: the macro `ts_project_macro` will set these outputs based on user
Expand All @@ -57,6 +56,19 @@ def _join(*elements):
return "/".join(segments)
return "."

def _relative_to_package(path, ctx):
for prefix in (ctx.bin_dir.path, ctx.label.package):
prefix += "/"
if path.startswith(prefix):
path = path[len(prefix):]
return path

def _declare_outputs(ctx, paths):
return [
ctx.actions.declare_file(path)
for path in paths
]

def _calculate_root_dir(ctx):
some_generated_path = None
some_source_path = None
Expand Down Expand Up @@ -91,6 +103,19 @@ def _calculate_root_dir(ctx):
)

def _ts_project_impl(ctx):
srcs = [_relative_to_package(src.path, ctx) for src in ctx.files.srcs]

# Recalculate outputs inside the rule implementation.
# The outs are first calculated in the macro in order to try to predetermine outputs so they can be declared as
# outputs on the rule. This provides the benefit of being able to reference an output file with a label.
# However, it is not possible to evaluate files in outputs of other rules such as filegroup, therefore the outs are
# recalculated here.
typings_out_dir = ctx.attr.declaration_dir or ctx.attr.out_dir
js_outs = _declare_outputs(ctx, [] if not ctx.attr.transpile else _calculate_js_outs(srcs, ctx.attr.out_dir, ctx.attr.root_dir, ctx.attr.allow_js, ctx.attr.preserve_jsx, ctx.attr.emit_declaration_only))
map_outs = _declare_outputs(ctx, [] if not ctx.attr.transpile else _calculate_map_outs(srcs, ctx.attr.out_dir, ctx.attr.root_dir, ctx.attr.source_map, ctx.attr.preserve_jsx, ctx.attr.emit_declaration_only))
typings_outs = _declare_outputs(ctx, _calculate_typings_outs(srcs, typings_out_dir, ctx.attr.root_dir, ctx.attr.declaration, ctx.attr.composite, ctx.attr.allow_js))
typing_maps_outs = _declare_outputs(ctx, _calculate_typing_maps_outs(srcs, typings_out_dir, ctx.attr.root_dir, ctx.attr.declaration_map, ctx.attr.allow_js))

arguments = ctx.actions.args()
execution_requirements = {}
progress_prefix = "Compiling TypeScript project"
Expand All @@ -114,7 +139,7 @@ def _ts_project_impl(ctx):
"--rootDir",
_calculate_root_dir(ctx),
])
if len(ctx.outputs.typings_outs) > 0:
if len(typings_outs) > 0:
declaration_dir = ctx.attr.declaration_dir if ctx.attr.declaration_dir else ctx.attr.out_dir
arguments.add_all([
"--declarationDir",
Expand Down Expand Up @@ -162,7 +187,7 @@ def _ts_project_impl(ctx):
# However tsc will copy .json srcs to the output tree so we want to declare these outputs to include along with .js Default outs
# NB: We don't have emit_declaration_only setting here, so use presence of any JS outputs as an equivalent.
# tsc will only produce .json if it also produces .js
if len(ctx.outputs.js_outs):
if len(js_outs):
pkg_len = len(ctx.label.package) + 1 if len(ctx.label.package) else 0
json_outs = [
ctx.actions.declare_file(_join(ctx.attr.out_dir, src.short_path[pkg_len:]))
Expand All @@ -172,15 +197,30 @@ def _ts_project_impl(ctx):
else:
json_outs = []

outputs = json_outs + ctx.outputs.js_outs + ctx.outputs.map_outs + ctx.outputs.typings_outs + ctx.outputs.typing_maps_outs
outputs = json_outs + js_outs + map_outs + typings_outs + typing_maps_outs
if ctx.outputs.buildinfo_out:
arguments.add_all([
"--tsBuildInfoFile",
ctx.outputs.buildinfo_out.path,
])
outputs.append(ctx.outputs.buildinfo_out)
runtime_outputs = json_outs + ctx.outputs.js_outs + ctx.outputs.map_outs
typings_outputs = ctx.outputs.typings_outs + ctx.outputs.typing_maps_outs + [s for s in ctx.files.srcs if s.path.endswith(".d.ts")]
runtime_outputs = json_outs + js_outs + map_outs
typings_outputs = typings_outs + typing_maps_outs + [s for s in ctx.files.srcs if s.path.endswith(".d.ts")]

if not js_outs and not typings_outputs and not ctx.attr.deps:
label = "//{}:{}".format(ctx.label.package, ctx.label.name)
if ctx.attr.transpile:
no_outs_msg = """ts_project target %s is configured to produce no outputs.
This might be because
- you configured it with `noEmit`
- the `srcs` are empty
""" % label
else:
no_outs_msg = "ts_project target %s with custom transpiler needs `declaration = True`." % label
fail(no_outs_msg + """
This is an error because Bazel does not run actions unless their outputs are needed for the requested targets to build.
""")

if ctx.attr.transpile:
default_outputs_depset = depset(runtime_outputs) if len(runtime_outputs) else depset(typings_outputs)
Expand Down Expand Up @@ -274,21 +314,59 @@ def _replace_ext(f, ext_map):
return new_ext
return None

def _out_paths(srcs, outdir, rootdir, allow_js, ext_map):
rootdir_replace_pattern = rootdir + "/" if rootdir else ""
def _out_paths(srcs, out_dir, root_dir, allow_js, ext_map):
rootdir_replace_pattern = root_dir + "/" if root_dir else ""
outs = []
for f in srcs:
if _is_ts_src(f, allow_js):
out = _join(outdir, f[:f.rindex(".")].replace(rootdir_replace_pattern, "") + _replace_ext(f, ext_map))
out = _join(out_dir, f[:f.rindex(".")].replace(rootdir_replace_pattern, "") + _replace_ext(f, ext_map))

# Don't declare outputs that collide with inputs
# for example, a.js -> a.js
if out != f:
outs.append(out)
return outs

def _calculate_js_outs(srcs, out_dir, root_dir, allow_js, preserve_jsx, emit_declaration_only):
if emit_declaration_only:
return []

exts = {
"*": ".js",
".jsx": ".jsx",
".tsx": ".jsx",
} if preserve_jsx else {"*": ".js"}
return _out_paths(srcs, out_dir, root_dir, allow_js, exts)

def _calculate_map_outs(srcs, out_dir, root_dir, source_map, preserve_jsx, emit_declaration_only):
if not source_map or emit_declaration_only:
return []

exts = {
"*": ".js.map",
".tsx": ".jsx.map",
} if preserve_jsx else {"*": ".js.map"}
return _out_paths(srcs, out_dir, root_dir, False, exts)

def _calculate_typings_outs(srcs, typings_out_dir, root_dir, declaration, composite, allow_js, include_srcs = True):
if not (declaration or composite):
return []

return _out_paths(srcs, typings_out_dir, root_dir, allow_js, {"*": ".d.ts"})

def _calculate_typing_maps_outs(srcs, typings_out_dir, root_dir, declaration_map, allow_js):
if not declaration_map:
return []

exts = {"*": ".d.ts.map"}
return _out_paths(srcs, typings_out_dir, root_dir, allow_js, exts)

lib = struct(
is_ts_src = _is_ts_src,
is_json_src = _is_json_src,
out_paths = _out_paths,
calculate_js_outs = _calculate_js_outs,
calculate_map_outs = _calculate_map_outs,
calculate_typings_outs = _calculate_typings_outs,
calculate_typing_maps_outs = _calculate_typing_maps_outs,
)
29 changes: 17 additions & 12 deletions packages/typescript/internal/validate_options.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,31 @@ def _validate_options_impl(ctx):
ValidOptionsInfo(marker = marker),
]

# These attrs are shared between the validate and the ts_project rules
SHARED_ATTRS = {
"allow_js": attr.bool(),
"composite": attr.bool(),
"declaration": attr.bool(),
"declaration_map": attr.bool(),
"emit_declaration_only": attr.bool(),
"extends": attr.label(allow_files = [".json"]),
"incremental": attr.bool(),
"preserve_jsx": attr.bool(),
"resolve_json_module": attr.bool(),
"source_map": attr.bool(),
}

validate_options = rule(
implementation = _validate_options_impl,
attrs = {
"allow_js": attr.bool(),
"composite": attr.bool(),
"declaration": attr.bool(),
"declaration_map": attr.bool(),
"emit_declaration_only": attr.bool(),
"extends": attr.label(allow_files = [".json"]),
"incremental": attr.bool(),
"preserve_jsx": attr.bool(),
"resolve_json_module": attr.bool(),
"source_map": attr.bool(),
attrs = dict(SHARED_ATTRS, **{
"target": attr.string(),
"ts_build_info_file": attr.string(),
"tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]),
"validator": attr.label(default = Label("//packages/typescript/bin:ts_project_options_validator"), executable = True, cfg = "exec"),
},
}),
)

lib = struct(
tsconfig_inputs = _tsconfig_inputs,
attrs = SHARED_ATTRS,
)
2 changes: 1 addition & 1 deletion packages/typescript/test/ts_project/c/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
],
// NB: we don't need the compilerOptions.outDir Windows workaround here
// because there is no project that references this one.
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Use the ts_project rule directly, not the wrapper macro. We don't want checking for empty outs.
load("//packages/typescript/internal:ts_project.bzl", "ts_project")
load("//packages/typescript:index.bzl", "ts_project")

ts_project(
name = "tsconfig-a",
Expand All @@ -18,7 +17,6 @@ ts_project(
ts_project(
name = "tsconfig-c",
srcs = ["c.ts"],
js_outs = ["c.js"],
tsconfig = ":tsconfig-c.json",
deps = ["tsconfig-b"],
)
14 changes: 14 additions & 0 deletions packages/typescript/test/ts_project/filegroup/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
load("//packages/typescript:index.bzl", "ts_project")

filegroup(
name = "srcs",
srcs = glob(["*.ts"]),
)

ts_project(
srcs = [":srcs"],
composite = True,
extends = "//packages/typescript/test/ts_project:tsconfig-base.json",
tsconfig = "tsconfig.json",
visibility = ["//packages/typescript/test:__subpackages__"],
)
1 change: 1 addition & 0 deletions packages/typescript/test/ts_project/filegroup/c.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("Hello Filegroup!");
3 changes: 3 additions & 0 deletions packages/typescript/test/ts_project/filegroup/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../tsconfig-base.json",
}

0 comments on commit 96d37b6

Please sign in to comment.