Skip to content

Commit 880df49

Browse files
committed
build: simplify api-golden rule and make it rules_js compatible
Simplifies the api-golden rule by removing the single file mode, and using synthethic npm packages here. In addition, this commit makes the rule compatible with `rules_js`, by providing another `_rjs.bzl` file, that will eventually take over the legacy exports that currently call into the `rjs` variant! (so in practice this rule is alrady using rjs and can work with `rules_nodejs` as an interop layer)
1 parent d20ec44 commit 880df49

File tree

11 files changed

+166
-219
lines changed

11 files changed

+166
-219
lines changed

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ github-actions/unified-status-check/main.js
2525
.github/local-actions/changelog/main.js
2626

2727
bazel/map-size-tracking/test/size-golden.json
28+
29+
bazel/pnpm-lock.yaml

bazel/api-golden/BUILD.bazel

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//bazel:defaults.bzl", "ts_library")
1+
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
22

33
package(default_visibility = ["//visibility:public"])
44

@@ -7,25 +7,21 @@ exports_files([
77
"index_npm_packages.ts",
88
])
99

10-
ts_library(
10+
ts_project(
1111
name = "api-golden",
1212
srcs = [
1313
"find_entry_points.ts",
14-
"index.ts",
1514
"index_npm_packages.ts",
1615
"module_mappings.ts",
1716
"patch-host.ts",
1817
"path-normalize.ts",
1918
"test_api_report.ts",
2019
],
21-
# A tsconfig needs to be specified as otherwise `ts_library` will look for the config
22-
# in `//:package.json` and this breaks when the BUILD file is copied to `@npm//`.
23-
tsconfig = "//:tsconfig.json",
20+
tsconfig = "//bazel:tsconfig",
2421
deps = [
25-
"@npm//@bazel/runfiles",
26-
"@npm//@microsoft/api-extractor",
27-
"@npm//@types/node",
28-
"@npm//typescript",
22+
"//bazel:node_modules/@microsoft/api-extractor",
23+
"//bazel:node_modules/@types/node",
24+
"//bazel:node_modules/typescript",
2925
],
3026
)
3127

bazel/api-golden/find_entry_points.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {dirname, join, relative} from 'path';
1010
import {lstatSync, readFileSync, readdirSync} from 'fs';
1111

12-
import {PackageJson} from './index_npm_packages';
12+
import {PackageJson} from './index_npm_packages.js';
1313

1414
/** Interface describing a resolved NPM package entry point. */
1515
export interface PackageEntryPoint {

bazel/api-golden/index.bzl

Lines changed: 38 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
load("//bazel:extract_types.bzl", "extract_types")
2-
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary", "nodejs_test")
1+
load("@bazel_skylib//rules:write_file.bzl", "write_file")
2+
load("@build_bazel_rules_nodejs//:index.bzl", "pkg_npm")
3+
load("//bazel/api-golden:index_rjs.bzl", _rjs_api_golden_test_npm_package = "api_golden_test_npm_package")
34

45
nodejs_test_args = [
56
# Needed so that node doesn't walk back to the source directory.
@@ -10,18 +11,8 @@ nodejs_test_args = [
1011

1112
default_strip_export_pattern = "^ɵ(?!ɵdefineInjectable|ɵinject|ɵInjectableDef)"
1213

13-
def _escape_regex_for_arg(value):
14-
"""Escapes a Regular expression so that it can be passed as process argument."""
15-
return "\"%s\"" % value
16-
1714
def extract_module_names_from_npm_targets(type_targets):
18-
"""Extracts the module names from a list of NPM targets.
19-
20-
For example: Consider the `@npm//@types/node` target. This function extracts
21-
`@types/node` from the label. This is needed so that the Node types can be
22-
resolved from within the test runner through runfile resolution.
23-
"""
24-
module_names = []
15+
types = {}
2516

2617
for type_target in type_targets:
2718
type_label = Label(type_target)
@@ -31,9 +22,9 @@ def extract_module_names_from_npm_targets(type_targets):
3122
fail("Expected type targets to be part of the `@npm` workspace." +
3223
"e.g. `@npm//@types/nodes`.")
3324

34-
module_names.append(type_package)
25+
types[type_target] = type_package
3526

36-
return module_names
27+
return types
3728

3829
def api_golden_test(
3930
name,
@@ -43,52 +34,33 @@ def api_golden_test(
4334
strip_export_pattern = default_strip_export_pattern,
4435
types = [],
4536
**kwargs):
46-
"""Builds an API report for the specified entry-point and compares it against the
47-
specified golden
48-
49-
Args;
50-
name: Name of the test target
51-
golden: Manifest path to the golden file
52-
entry_point: Manifest path to the type definition entry-point.
53-
data: Runtime dependenices needed for the rule (e.g. transitive type definitions)
54-
strip_export_pattern: An optional regular expression to filter out exports from the golden.
55-
types: Optional list of type targets to make available in the API report generation.
56-
"""
57-
58-
quoted_export_pattern = _escape_regex_for_arg(strip_export_pattern)
59-
60-
kwargs["tags"] = kwargs.get("tags", []) + ["api_guard"]
37+
write_file(
38+
name = "%s_synthetic_package_json" % name,
39+
out = "package.json",
40+
content = [json.encode({
41+
"name": name,
42+
"exports": {
43+
".": {
44+
"types": entry_point,
45+
},
46+
},
47+
})],
48+
)
6149

62-
# For API golden tests not running against a NPM package, we extract all transitive
63-
# declarations of the specified `data` targets. This is necessary because API extractor
64-
# needs to resolve other targets that have been linked by the Bazel NodeJS rules. The
65-
# linker by default only provides access to JavaScript sources, but the API extractor is
66-
# specifically concerned with type definitions that we can extract manually here.
67-
extract_types(
68-
name = "%s_data_typings" % name,
50+
pkg_npm(
51+
name = "%s_synthetic_package" % name,
52+
deps = data + ["%s_synthetic_package_json" % name],
6953
testonly = True,
70-
deps = data,
7154
)
7255

73-
test_data = ["//bazel/api-golden", "//:package.json", ":%s_data_typings" % name] + \
74-
data + types
75-
76-
nodejs_test(
56+
_rjs_api_golden_test_npm_package(
57+
no_copy_to_bin = types,
7758
name = name,
78-
data = test_data,
79-
entry_point = "//bazel/api-golden:index.ts",
80-
templated_args = nodejs_test_args + [golden, entry_point, "false", quoted_export_pattern] +
81-
extract_module_names_from_npm_targets(types),
82-
**kwargs
83-
)
84-
85-
nodejs_binary(
86-
name = name + ".accept",
87-
testonly = True,
88-
data = test_data,
89-
entry_point = "//bazel/api-golden:index.ts",
90-
templated_args = nodejs_test_args + [golden, entry_point, "true", quoted_export_pattern] +
91-
extract_module_names_from_npm_targets(types),
59+
golden_dir = fixup_path_for_rules_js(golden),
60+
data = [":%s_synthetic_package" % name] + data,
61+
npm_package = "%s/%s_synthetic_package" % (native.package_name(), name),
62+
strip_export_pattern = strip_export_pattern,
63+
types = extract_module_names_from_npm_targets(types),
9264
**kwargs
9365
)
9466

@@ -100,37 +72,17 @@ def api_golden_test_npm_package(
10072
strip_export_pattern = default_strip_export_pattern,
10173
types = [],
10274
**kwargs):
103-
"""Builds an API report for all entry-points within the given NPM package and compares it
104-
against goldens within the specified directory.
105-
106-
Args;
107-
name: Name of the test target
108-
golden_dir: Manifest path to the golden directory
109-
npm_package: Manifest path to the NPM package.
110-
data: Runtime dependenices needed for the rule (e.g. the tree artifact of the NPM package)
111-
strip_export_pattern: An optional regular expression to filter out exports from the golden.
112-
types: Optional list of type targets to make available in the API report generation.
113-
"""
114-
115-
quoted_export_pattern = _escape_regex_for_arg(strip_export_pattern)
116-
117-
kwargs["tags"] = kwargs.get("tags", []) + ["api_guard"]
118-
119-
nodejs_test(
75+
_rjs_api_golden_test_npm_package(
12076
name = name,
121-
data = ["//bazel/api-golden"] + data + types,
122-
entry_point = "//bazel/api-golden:index_npm_packages.ts",
123-
templated_args = nodejs_test_args + [golden_dir, npm_package, "false", quoted_export_pattern] +
124-
extract_module_names_from_npm_targets(types),
77+
no_copy_to_bin = types,
78+
golden_dir = fixup_path_for_rules_js(golden_dir),
79+
npm_package = fixup_path_for_rules_js(npm_package),
80+
data = data,
81+
strip_export_pattern = strip_export_pattern,
82+
types = extract_module_names_from_npm_targets(types),
12583
**kwargs
12684
)
12785

128-
nodejs_binary(
129-
name = name + ".accept",
130-
testonly = True,
131-
data = ["//bazel/api-golden"] + data + types,
132-
entry_point = "//bazel/api-golden:index_npm_packages.ts",
133-
templated_args = nodejs_test_args + [golden_dir, npm_package, "true", quoted_export_pattern] +
134-
extract_module_names_from_npm_targets(types),
135-
**kwargs
136-
)
86+
def fixup_path_for_rules_js(p):
87+
segs = p.split("/")
88+
return "/".join(segs[1:])

bazel/api-golden/index.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

bazel/api-golden/index_npm_packages.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {findEntryPointsWithinNpmPackage} from './find_entry_points';
10-
import {join} from 'path';
11-
import {normalizePathToPosix} from './path-normalize';
9+
import {findEntryPointsWithinNpmPackage} from './find_entry_points.js';
10+
import * as path from 'path';
11+
import {normalizePathToPosix} from './path-normalize.js';
1212
import {readFileSync} from 'fs';
13-
import {runfiles} from '@bazel/runfiles';
14-
import {testApiGolden} from './test_api_report';
13+
import {testApiGolden} from './test_api_report.js';
14+
import * as fs from 'fs';
1515

1616
/** Interface describing contents of a `package.json`. */
1717
export interface PackageJson {
@@ -37,7 +37,7 @@ async function main(
3737
// guaranteed to be ESM-only and supports the `mts` extension.
3838
const chalk = {red: (v: string) => v, yellow: (v: string) => v};
3939

40-
const packageJsonPath = join(npmPackageDir, 'package.json');
40+
const packageJsonPath = path.join(npmPackageDir, 'package.json');
4141
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJson;
4242
const entryPoints = findEntryPointsWithinNpmPackage(npmPackageDir, packageJson);
4343
const outdatedGoldens: string[] = [];
@@ -49,26 +49,34 @@ async function main(
4949
// entry-point we maintain a separate golden file. These golden files are
5050
// based on the name of the defining NodeJS exports subpath in the NPM package,
5151
// See: https://api-extractor.com/pages/overview/demo_api_report/.
52-
const goldenName = join(subpath, 'index.api.md');
53-
const goldenFilePath = join(goldenDir, goldenName);
54-
const moduleName = normalizePathToPosix(join(packageJson.name, subpath));
52+
const goldenName = path.join(subpath, 'index.api.md');
53+
const goldenFilePath = path.join(goldenDir, goldenName);
54+
const moduleName = normalizePathToPosix(path.join(packageJson.name, subpath));
5555

56-
const {succeeded, apiReportChanged} = await testApiGolden(
57-
goldenFilePath,
56+
const expected = fs.readFileSync(goldenFilePath, 'utf8');
57+
const actual = await testApiGolden(
5858
typesEntryPointPath,
59-
approveGolden,
6059
stripExportPattern,
6160
typePackageNames,
6261
packageJsonPath,
6362
moduleName,
6463
);
6564

65+
if (actual === null) {
66+
console.error(`Could not generate API golden for subpath: "${subpath}". See errors above.`);
67+
process.exit(1);
68+
}
69+
6670
// Keep track of outdated goldens.
67-
if (!succeeded && apiReportChanged) {
68-
outdatedGoldens.push(goldenName);
71+
if (actual !== expected) {
72+
if (approveGolden) {
73+
fs.writeFileSync(goldenFilePath, actual, 'utf8');
74+
} else {
75+
outdatedGoldens.push(goldenName);
76+
}
6977
}
7078

71-
allTestsSucceeding = allTestsSucceeding && succeeded;
79+
allTestsSucceeding = allTestsSucceeding && actual === expected;
7280
}
7381

7482
if (outdatedGoldens.length) {
@@ -89,12 +97,17 @@ async function main(
8997
// Invoke main.
9098
(() => {
9199
const args = process.argv.slice(2);
92-
const goldenDir = runfiles.resolve(args[0]);
93-
const npmPackageDir = runfiles.resolve(args[1]);
100+
let goldenDir = path.resolve(args[0]);
101+
const npmPackageDir = path.resolve(args[1]);
94102
const approveGolden = args[2] === 'true';
95103
const stripExportPattern = new RegExp(args[3]);
96104
const typePackageNames = args.slice(4);
97105

106+
// For approving, point to the real directory outside of the bazel-out.
107+
if (approveGolden) {
108+
goldenDir = path.join(process.env.BUILD_WORKSPACE_DIRECTORY!, args[0]);
109+
}
110+
98111
main(goldenDir, npmPackageDir, approveGolden, stripExportPattern, typePackageNames).catch((e) => {
99112
console.error(e);
100113
process.exit(1);

0 commit comments

Comments
 (0)