Skip to content

Commit b3dc431

Browse files
committed
fix(bazel): handle additional cases for strict deps testing
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
1 parent 1ef1e98 commit b3dc431

File tree

9 files changed

+126
-13
lines changed

9 files changed

+126
-13
lines changed

MODULE.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
8080
npm.npm_translate_lock(
8181
name = "devinfra_npm",
8282
data = [
83+
"@devinfra//:package.json",
84+
"@devinfra//:pnpm-workspace.yaml",
8385
"@devinfra//.github/local-actions:package.json",
8486
"@devinfra//.github/local-actions/branch-manager:package.json",
8587
"@devinfra//.github/local-actions/labels-sync:package.json",
8688
"@devinfra//.github/local-actions/lock-closed:package.json",
87-
"@devinfra//:package.json",
88-
"@devinfra//:pnpm-workspace.yaml",
8989
"@devinfra//apps/code-of-conduct:package.json",
9090
"@devinfra//apps/functions:package.json",
9191
"@devinfra//bazel:package.json",

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)