Skip to content

Commit acce054

Browse files
committed
fix(bazel): handle additional cases for strict deps testing (#2988)
Properly handle module names that come with packages in DefinitelyTyped Properly handle module names that are described within tsconfig paths configuration Properly handle edge case of `zone.js` containing an extension in its name PR Close #2988
1 parent 1ef1e98 commit acce054

File tree

8 files changed

+124
-11
lines changed

8 files changed

+124
-11
lines changed

bazel/ts_project/strict_deps/diagnostic.mts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
19
import ts from 'typescript';
210

311
export function createDiagnostic(message: string, node: ts.Node): ts.Diagnostic {

bazel/ts_project/strict_deps/index.bzl

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def _strict_deps_impl(ctx):
5353
"testFiles": test_files,
5454
"allowedModuleNames": allowed_module_names,
5555
"allowedSources": allowed_sources,
56+
# The tsconfig from rules_ts has a single src so we know it will be the first file.
57+
"tsconfigPath": ctx.files.tsconfig[0].short_path,
5658
}),
5759
)
5860

@@ -85,14 +87,21 @@ def _strict_deps_impl(ctx):
8587
),
8688
)
8789

88-
bin_runfiles = ctx.attr._bin[DefaultInfo].default_runfiles
90+
runfiles = ctx.runfiles(
91+
files = [
92+
manifest,
93+
] + ctx.files.srcs +
94+
ctx.files._runfiles_lib +
95+
ctx.files.tsconfig,
96+
).merge_all([
97+
ctx.attr._bin[DefaultInfo].default_runfiles,
98+
ctx.attr.tsconfig[DefaultInfo].default_runfiles,
99+
])
89100

90101
return [
91102
DefaultInfo(
92103
executable = launcher,
93-
runfiles = ctx.runfiles(
94-
files = ctx.files._runfiles_lib + ctx.files.srcs + [manifest],
95-
).merge(bin_runfiles),
104+
runfiles = runfiles,
96105
),
97106
]
98107

@@ -108,8 +117,13 @@ _strict_deps_test = rule(
108117
),
109118
"srcs": attr.label_list(
110119
doc = "TS files to be checked",
120+
mandatory = True,
111121
allow_files = True,
122+
),
123+
"tsconfig": attr.label(
124+
doc = "The tsconfig of the ts_project being checked",
112125
mandatory = True,
126+
allow_files = True,
113127
),
114128
"will_fail": attr.bool(
115129
doc = "Whether the test is expected to fail",

bazel/ts_project/strict_deps/index.mts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {isBuiltin} from 'node:module';
110
import fs from 'node:fs/promises';
211
import path from 'node:path';
312
import ts from 'typescript';
413
import {createDiagnostic} from './diagnostic.mjs';
514
import {StrictDepsManifest} from './manifest.mjs';
615
import {getImportsInSourceFile} from './visitor.mjs';
16+
import {readTsConfig} from './tsconfig.mjs';
717

818
const [manifestExecPath, expectedFailureRaw] = process.argv.slice(2);
919
const expectedFailure = expectedFailureRaw === 'true';
@@ -13,22 +23,45 @@ const manifest: StrictDepsManifest = JSON.parse(await fs.readFile(manifestExecPa
1323
/**
1424
* Regex matcher to extract a npm package name, potentially with scope from a subpackage import path.
1525
*/
16-
const moduleSpeciferMatcher = /^(@[\w\d-_]+\/)?([\w\d-_]+)/;
17-
const extensionRemoveRegex = /\.[mc]?(js|(d\.)?[mc]?ts)$/;
18-
const allowedModuleNames = new Set<string>(manifest.allowedModuleNames);
26+
const moduleSpeciferMatcher = /^(@[\w\d-_\.]+\/)?([\w\d-_\.]+)/;
27+
const extensionRemoveRegex = /\.[mc]?(js|(d\.)?[mc]?tsx?)$/;
28+
const allowedModuleNames = new Set<string>(
29+
manifest.allowedModuleNames.map((m) => {
30+
return (
31+
m
32+
// Scoped types from DefinitelyTyped are split using a __ delimiter, so we put it back together.
33+
.replace(/(?:@types\/)(.*)__(.*)/, '@$1/$2')
34+
// Replace any unscoped types package from DefinitelyTyped with just to package name.
35+
.replace(/(?:@types\/)(.*)/, '$1')
36+
);
37+
}),
38+
);
1939
const allowedSources = new Set<string>(
2040
manifest.allowedSources.map((s) => s.replace(extensionRemoveRegex, '')),
2141
);
22-
42+
const tsconfig = readTsConfig(path.join(process.cwd(), manifest.tsconfigPath));
2343
const diagnostics: ts.Diagnostic[] = [];
2444

45+
/** Check if the moduleSpecifier matches any of the provided paths. */
46+
function checkPathsForMatch(moduleSpecifier: string, paths?: ts.MapLike<string[]>): boolean {
47+
for (const matcher of Object.keys(paths || {})) {
48+
if (new RegExp(matcher).test(moduleSpecifier)) {
49+
return true;
50+
}
51+
}
52+
return false;
53+
}
54+
2555
for (const fileExecPath of manifest.testFiles) {
2656
const content = await fs.readFile(fileExecPath, 'utf8');
2757
const sf = ts.createSourceFile(fileExecPath, content, ts.ScriptTarget.ESNext, true);
2858
const imports = getImportsInSourceFile(sf);
2959

3060
for (const i of imports) {
31-
const moduleSpecifier = i.moduleSpecifier.replace(extensionRemoveRegex, '');
61+
const moduleSpecifier =
62+
i.moduleSpecifier === 'zone.js'
63+
? 'zone.js'
64+
: i.moduleSpecifier.replace(extensionRemoveRegex, '');
3265
// When the module specified is the file itself this is always a valid dep.
3366
if (i.moduleSpecifier === '') {
3467
continue;
@@ -44,16 +77,24 @@ for (const fileExecPath of manifest.testFiles) {
4477
}
4578
}
4679

47-
if (moduleSpecifier.startsWith('node:') && allowedModuleNames.has('@types/node')) {
80+
if (
81+
isBuiltin(moduleSpecifier) &&
82+
(allowedModuleNames.has('node') || tsconfig.options.types?.includes('node'))
83+
) {
4884
continue;
4985
}
5086

5187
if (
52-
allowedModuleNames.has(moduleSpecifier.match(moduleSpeciferMatcher)?.[0] || moduleSpecifier)
88+
allowedModuleNames.has(moduleSpecifier.match(moduleSpeciferMatcher)?.[0] || '') ||
89+
allowedModuleNames.has(moduleSpecifier)
5390
) {
5491
continue;
5592
}
5693

94+
if (checkPathsForMatch(moduleSpecifier, tsconfig.options.paths)) {
95+
continue;
96+
}
97+
5798
diagnostics.push(
5899
createDiagnostic(`No explicit Bazel dependency for this module.`, i.diagnosticNode),
59100
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
19
export interface StrictDepsManifest {
210
allowedModuleNames: string[];
311
allowedSources: string[];
412
testFiles: string[];
13+
tsconfigPath: string;
514
}

bazel/ts_project/strict_deps/test/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_project(
1212
strict_deps_test(
1313
name = "import_node_module",
1414
srcs = ["import_node_module.ts"],
15+
tsconfig = "//bazel:tsconfig",
1516
deps = [
1617
"//bazel:node_modules/@types/node",
1718
],
@@ -20,17 +21,20 @@ strict_deps_test(
2021
invalid_strict_deps_test(
2122
name = "invalid_import_node_module",
2223
srcs = ["import_node_module.ts"],
24+
tsconfig = "//bazel:tsconfig",
2325
)
2426

2527
strict_deps_test(
2628
name = "import_npm_module",
2729
srcs = ["import_npm_module.ts"],
30+
tsconfig = "//bazel:tsconfig",
2831
deps = ["//bazel:node_modules/@microsoft/api-extractor"],
2932
)
3033

3134
invalid_strict_deps_test(
3235
name = "invalid_import_npm_module_transitively",
3336
srcs = ["import_npm_module.ts"],
37+
tsconfig = "//bazel:tsconfig",
3438
deps = [
3539
"//bazel/ts_project/strict_deps/test/import_npm_module",
3640
],
@@ -39,17 +43,20 @@ invalid_strict_deps_test(
3943
invalid_strict_deps_test(
4044
name = "invalid_import_npm_module",
4145
srcs = ["import_npm_module.ts"],
46+
tsconfig = "//bazel:tsconfig",
4247
)
4348

4449
strict_deps_test(
4550
name = "import_from_depth",
4651
srcs = ["import_from_depth.ts"],
52+
tsconfig = "//bazel:tsconfig",
4753
deps = ["//bazel/ts_project/strict_deps/test/depth"],
4854
)
4955

5056
invalid_strict_deps_test(
5157
name = "invalid_import_from_depth",
5258
srcs = ["import_from_depth.ts"],
59+
tsconfig = "//bazel:tsconfig",
5360
deps = [
5461
":sibling_import_from_depth",
5562
],

bazel/ts_project/strict_deps/test/import_from_mts_cts_extensions/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ load("//bazel/ts_project/strict_deps:index.bzl", "strict_deps_test")
44
strict_deps_test(
55
name = "import_from_mts_cts_extensions",
66
srcs = ["index.ts"],
7+
tsconfig = "//bazel:tsconfig",
78
deps = [":mts_cts_extensions"],
89
)
910

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
import {dirname} from 'path';
11+
12+
export function readTsConfig(filePath: string) {
13+
const configFile = ts.readConfigFile(filePath, ts.sys.readFile);
14+
if (configFile.error) {
15+
throw new Error(ts.formatDiagnostics([configFile.error], ts.createCompilerHost({})));
16+
}
17+
18+
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, dirname(filePath));
19+
20+
if (parsedConfig.errors.length > 0) {
21+
throw new Error(ts.formatDiagnostics(parsedConfig.errors, ts.createCompilerHost({})));
22+
}
23+
24+
return parsedConfig;
25+
}

bazel/ts_project/strict_deps/visitor.mts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
19
import ts from 'typescript';
210

311
export interface Import {

0 commit comments

Comments
 (0)