Skip to content

Commit 8df86cc

Browse files
authored
feat(typescript): warn the user when rootDirs looks wrong in ts_proje… (bazel-contrib#3126)
* feat(typescript): warn the user when rootDirs looks wrong in ts_project tsconfig This should help prevent users from falling into confusing typescript failures as often * code review comments
1 parent 550673f commit 8df86cc

File tree

4 files changed

+85
-5
lines changed

4 files changed

+85
-5
lines changed

docs/TypeScript.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,16 @@ Any code that works with `tsc` should work with `ts_project` with a few caveats:
248248
Worse, if you build them separately then the output directory will contain whichever
249249
one you happened to build most recently. This is highly discouraged.
250250

251+
As a thin wrapper, this rule doesn't try to compensate for behavior of the TypeScript compiler.
252+
See https://github.com/bazelbuild/rules_nodejs/wiki/Debugging-problems-with-ts_project for notes
253+
that may help you debug issues.
254+
251255
> Note: in order for TypeScript to resolve relative references to the bazel-out folder,
252256
> we recommend that the base tsconfig contain a rootDirs section that includes all
253257
> possible locations they may appear.
254258
>
255259
> We hope this will not be needed in some future release of TypeScript.
256-
> Follow https://github.com/microsoft/TypeScript/issues/37257 for more info.
260+
> Follow https://github.com/microsoft/TypeScript/issues/37378 for more info.
257261
>
258262
> For example, if the base tsconfig file relative to the workspace root is
259263
> `path/to/tsconfig.json` then you should configure like:

packages/typescript/index.bzl

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,50 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test")
3030

3131
ts_config = _ts_config
3232

33+
# Copied from aspect_bazel_lib
34+
# https://github.com/aspect-build/bazel-lib/blob/main/lib/private/utils.bzl#L73-L82
35+
# TODO(6.0): depend on that library and remove this copy
36+
def _to_label(param):
37+
"""Converts a string to a Label. If Label is supplied, the same label is returned.
38+
39+
Args:
40+
param: a string representing a label or a Label
41+
Returns:
42+
a Label
43+
"""
44+
param_type = type(param)
45+
if param_type == "string":
46+
if param.startswith("@"):
47+
return Label(param)
48+
if param.startswith("//"):
49+
return Label("@" + param)
50+
51+
# resolve the relative label from the current package
52+
# if 'param' is in another workspace, then this would return the label relative to that workspace, eg:
53+
# `Label("@my//foo:bar").relative("@other//baz:bill") == Label("@other//baz:bill")`
54+
if param.startswith(":"):
55+
param = param[1:]
56+
if native.package_name():
57+
return Label("@//" + native.package_name()).relative(param)
58+
else:
59+
return Label("@//:" + param)
60+
61+
elif param_type == "Label":
62+
return param
63+
else:
64+
fail("Expected 'string' or 'Label' but got '%s'" % param_type)
65+
66+
# copied from aspect_bazel_lib, see comment above
67+
def _is_external_label(param):
68+
"""Returns True if the given Label (or stringy version of a label) represents a target outside of the workspace
69+
70+
Args:
71+
param: a string or label
72+
Returns:
73+
a bool
74+
"""
75+
return len(_to_label(param).workspace_root) > 0
76+
3377
_DEFAULT_TYPESCRIPT_PACKAGE = (
3478
# BEGIN-INTERNAL
3579
"@npm" +
@@ -96,12 +140,16 @@ def ts_project(
96140
Worse, if you build them separately then the output directory will contain whichever
97141
one you happened to build most recently. This is highly discouraged.
98142
143+
As a thin wrapper, this rule doesn't try to compensate for behavior of the TypeScript compiler.
144+
See https://github.com/bazelbuild/rules_nodejs/wiki/Debugging-problems-with-ts_project for notes
145+
that may help you debug issues.
146+
99147
> Note: in order for TypeScript to resolve relative references to the bazel-out folder,
100148
> we recommend that the base tsconfig contain a rootDirs section that includes all
101149
> possible locations they may appear.
102150
>
103151
> We hope this will not be needed in some future release of TypeScript.
104-
> Follow https://github.com/microsoft/TypeScript/issues/37257 for more info.
152+
> Follow https://github.com/microsoft/TypeScript/issues/37378 for more info.
105153
>
106154
> For example, if the base tsconfig file relative to the workspace root is
107155
> `path/to/tsconfig.json` then you should configure like:
@@ -434,6 +482,7 @@ def ts_project(
434482
allow_js = allow_js,
435483
tsconfig = tsconfig,
436484
extends = extends,
485+
has_local_deps = len([d for d in deps if not _is_external_label(d)]) > 0,
437486
**common_kwargs
438487
)
439488
tsc_deps = tsc_deps + ["_validate_%s_options" % name]

packages/typescript/internal/ts_project_options_validator.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {relative} from 'path';
1+
import {dirname, relative} from 'path';
22
import * as ts from 'typescript';
33

44
const diagnosticsHost: ts.FormatDiagnosticsHost = {
@@ -19,12 +19,12 @@ function main([tsconfigPath, output, target, attrsStr]: string[]): 0|1 {
1919
const {config, error} = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
2020
if (error) throw new Error(tsconfigPath + ':' + ts.formatDiagnostic(error, diagnosticsHost));
2121
const {errors, options} =
22-
ts.parseJsonConfigFileContent(config, ts.sys, require('path').dirname(tsconfigPath));
22+
ts.parseJsonConfigFileContent(config, ts.sys, dirname(tsconfigPath));
2323
// We don't pass the srcs to this action, so it can't know if the program has the right sources.
2424
// Diagnostics look like
2525
// error TS18002: The 'files' list in config file 'tsconfig.json' is empty.
2626
// error TS18003: No inputs were found in config file 'tsconfig.json'. Specified 'include'...
27-
const fatalErrors = errors.filter(e => e.code !== 18002 && e.code != 18003);
27+
const fatalErrors = errors.filter(e => e.code !== 18002 && e.code !== 18003);
2828
if (fatalErrors.length > 0)
2929
throw new Error(tsconfigPath + ':' + ts.formatDiagnostics(fatalErrors, diagnosticsHost));
3030

@@ -93,6 +93,28 @@ function main([tsconfigPath, output, target, attrsStr]: string[]): 0|1 {
9393
console.error('- Otherwise, remove the noEmit option from tsconfig and try again.');
9494
return 1;
9595
}
96+
97+
// When there are dependencies on other ts_project targets, the tsconfig must be configured
98+
// to help TypeScript resolve them.
99+
if (attrs.has_local_deps) {
100+
let rootDirsValid = true
101+
if (!options.rootDirs) {
102+
console.error(`ERROR: ts_project rule ${target} is configured without rootDirs.`);
103+
rootDirsValid = false;
104+
}
105+
else if (!options.rootDirs.some(d => d.startsWith(process.env['BINDIR']))) {
106+
console.error(`ERROR: ts_project rule ${target} is missing a needed rootDir under ${process.env['BINDIR']}.`);
107+
console.error('Found only: ', options.rootDirs);
108+
rootDirsValid = false;
109+
}
110+
if (!rootDirsValid) {
111+
console.error('This makes it likely that TypeScript will be unable to resolve dependencies using relative import paths');
112+
console.error(`For example, if you 'import {} from ./foo', this expects to resolve foo.d.ts from Bazel's output tree`);
113+
console.error('and TypeScript only knows how to locate this when the rootDirs attribute includes that path.');
114+
console.error('See the ts_project documentation: https://bazelbuild.github.io/rules_nodejs/TypeScript.html#ts_project');
115+
return 1;
116+
}
117+
}
96118

97119
check('allowJs', 'allow_js');
98120
check('declarationMap', 'declaration_map');

packages/typescript/internal/validate_options.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def _validate_options_impl(ctx):
2929
allow_js = ctx.attr.allow_js,
3030
declaration = ctx.attr.declaration,
3131
declaration_map = ctx.attr.declaration_map,
32+
has_local_deps = ctx.attr.has_local_deps,
3233
preserve_jsx = ctx.attr.preserve_jsx,
3334
composite = ctx.attr.composite,
3435
emit_declaration_only = ctx.attr.emit_declaration_only,
@@ -47,6 +48,9 @@ def _validate_options_impl(ctx):
4748
outputs = [marker],
4849
arguments = [arguments],
4950
executable = "validator",
51+
env = {
52+
"BINDIR": ctx.var["BINDIR"],
53+
},
5054
)
5155
return [
5256
ValidOptionsInfo(marker = marker),
@@ -69,6 +73,7 @@ SHARED_ATTRS = {
6973
validate_options = rule(
7074
implementation = _validate_options_impl,
7175
attrs = dict(SHARED_ATTRS, **{
76+
"has_local_deps": attr.bool(doc = "Whether any of the deps are in the local workspace (not starting with '@')"),
7277
"target": attr.string(),
7378
"ts_build_info_file": attr.string(),
7479
"tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]),

0 commit comments

Comments
 (0)