Skip to content

Commit 0d3fba1

Browse files
committed
WIP: generate adev-compatible api json
1 parent 4e0ea8e commit 0d3fba1

File tree

7 files changed

+567
-0
lines changed

7 files changed

+567
-0
lines changed

src/cdk/testing/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")
22
load("//tools:defaults.bzl", "markdown_to_html", "ng_web_test_suite")
33
load("//src/cdk/testing/tests:webdriver-test.bzl", "webdriver_test")
44
load("//tools:defaults2.bzl", "ts_project")
5+
load("//tools/adev-api-extraction:extract_api_to_json.bzl", "extract_api_to_json")
56

67
package(default_visibility = ["//visibility:public"])
78

@@ -48,3 +49,14 @@ webdriver_test(
4849
"//src/cdk/testing/tests:webdriver_test_sources",
4950
],
5051
)
52+
53+
extract_api_to_json(
54+
name = "json-api",
55+
srcs = [
56+
":source-files",
57+
],
58+
entry_point = ":index.ts",
59+
module_name = "@angular/cdk/testing",
60+
output_name = "cdk_testing.json",
61+
private_modules = [""],
62+
)

src/cdk/testing/protractor/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("//tools:defaults2.bzl", "ts_project")
2+
load("//tools/adev-api-extraction:extract_api_to_json.bzl", "extract_api_to_json")
23

34
package(default_visibility = ["//visibility:public"])
45

@@ -20,3 +21,14 @@ filegroup(
2021
name = "source-files",
2122
srcs = glob(["**/*.ts"]),
2223
)
24+
25+
extract_api_to_json(
26+
name = "json-api",
27+
srcs = [
28+
":source-files",
29+
],
30+
entry_point = ":index.ts",
31+
module_name = "@angular/cdk/testing/protractor",
32+
output_name = "cdk_testing_protractor.json",
33+
private_modules = [""],
34+
)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
2+
load("@npm//@angular/build-tooling/bazel/esbuild:index.bzl", "esbuild_esm_bundle")
3+
load("//tools:defaults2.bzl", "ts_project")
4+
load("@aspect_rules_ts//ts:defs.bzl", rules_js_tsconfig = "ts_config")
5+
6+
package(default_visibility = ["//visibility:public"])
7+
8+
esbuild_esm_bundle(
9+
name = "bin",
10+
entry_point = ":index.ts",
11+
external = [
12+
"@angular/compiler-cli",
13+
"typescript",
14+
],
15+
output = "bin.mjs",
16+
platform = "node",
17+
target = "es2022",
18+
deps = [
19+
":extract_api_to_json_lib",
20+
"@npm//@angular/compiler-cli",
21+
"@npm//typescript",
22+
],
23+
)
24+
25+
ts_project(
26+
name = "extract_api_to_json_lib",
27+
srcs = glob(
28+
["**/*.ts"],
29+
exclude = [
30+
"**/*.spec.ts",
31+
],
32+
),
33+
resolve_json_module = True,
34+
tsconfig = ":tsconfig",
35+
deps = [
36+
"//:node_modules/@angular/compiler",
37+
"//:node_modules/@angular/compiler-cli",
38+
"//:node_modules/@bazel/runfiles",
39+
"//:node_modules/@types/node",
40+
"//:node_modules/typescript",
41+
],
42+
)
43+
44+
# Action binary for the api_gen bazel rule.
45+
nodejs_binary(
46+
name = "extract_api_to_json",
47+
data = [
48+
":bin",
49+
"@npm//@angular/compiler",
50+
"@npm//@angular/compiler-cli",
51+
"@npm//typescript",
52+
],
53+
entry_point = "bin.mjs",
54+
# Note: Using the linker here as we need it for ESM. The linker is not
55+
# super reliably when running concurrently on Windows- but we have existing
56+
# actions using the linker. An alternative would be to:
57+
# - bundle the Angular compiler into a CommonJS bundle
58+
# - use the patched resolution- but also patch the ESM imports (similar to how FW does it).
59+
visibility = ["//visibility:public"],
60+
)
61+
62+
# Expose the sources in the dev-infra NPM package.
63+
filegroup(
64+
name = "files",
65+
srcs = glob(["**/*"]),
66+
)
67+
68+
rules_js_tsconfig(
69+
name = "tsconfig",
70+
src = "tsconfig.json",
71+
deps = ["//:node_modules/@types/node"],
72+
)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
load("@build_bazel_rules_nodejs//:providers.bzl", "run_node")
2+
3+
def _extract_api_to_json(ctx):
4+
"""Implementation of the extract_api_to_json rule"""
5+
6+
# Define arguments that will be passed to the underlying nodejs program.
7+
args = ctx.actions.args()
8+
9+
# Use a param file because we may have a large number of inputs.
10+
args.set_param_file_format("multiline")
11+
args.use_param_file("%s", use_always = True)
12+
13+
# Pass the module_name for the extracted APIs. This will be something like "@angular/core".
14+
args.add(ctx.attr.module_name)
15+
16+
# Pass the module_label for the extracted APIs, This is something like core for "@angular/core".
17+
args.add(ctx.attr.module_label)
18+
19+
# Pass the set of private modules that should not be included in the API reference.
20+
args.add_joined(ctx.attr.private_modules, join_with = ",")
21+
22+
# Pass the entry_point for from which to extract public symbols.
23+
args.add(ctx.file.entry_point)
24+
25+
# Pass the set of source files from which API reference data will be extracted.
26+
args.add_joined(ctx.files.srcs, join_with = ",")
27+
28+
# Pass the name of the output JSON file.
29+
json_output = ctx.outputs.output_name
30+
args.add(json_output.path)
31+
32+
# Pass the import path map
33+
# TODO: consider module_mappings_aspect to deal with path mappings instead of manually
34+
# specifying them
35+
# https://github.com/bazelbuild/rules_nodejs/blob/5.x/internal/linker/link_node_modules.bzl#L236
36+
path_map = {}
37+
for target, path in ctx.attr.import_map.items():
38+
files = target.files.to_list()
39+
if len(files) != 1:
40+
fail("Expected a single file in import_map target %s" % target.label)
41+
path_map[path] = files[0].path
42+
args.add(json.encode(path_map))
43+
44+
# Pass the set of (optional) extra entries
45+
args.add_joined(ctx.files.extra_entries, join_with = ",")
46+
47+
# Define an action that runs the nodejs_binary executable. This is
48+
# the main thing that this rule does.
49+
run_node(
50+
ctx = ctx,
51+
inputs = depset(ctx.files.srcs + ctx.files.extra_entries),
52+
executable = "_extract_api_to_json",
53+
outputs = [json_output],
54+
arguments = [args],
55+
)
56+
57+
# The return value describes what the rule is producing. In this case we need to specify
58+
# the "DefaultInfo" with the output JSON files.
59+
return [DefaultInfo(files = depset([json_output]))]
60+
61+
extract_api_to_json = rule(
62+
# Point to the starlark function that will execute for this rule.
63+
implementation = _extract_api_to_json,
64+
doc = """Rule that extracts Angular API reference information from TypeScript
65+
sources and write it to a JSON file""",
66+
67+
# The attributes that can be set to this rule.
68+
attrs = {
69+
"srcs": attr.label_list(
70+
doc = """The source files for this rule. This must include one or more
71+
TypeScript files.""",
72+
allow_empty = False,
73+
allow_files = True,
74+
),
75+
"output_name": attr.output(
76+
doc = """Name of the JSON output file.""",
77+
),
78+
"entry_point": attr.label(
79+
doc = """Source file entry-point from which to extract public symbols""",
80+
mandatory = True,
81+
allow_single_file = True,
82+
),
83+
"private_modules": attr.string_list(
84+
doc = """List of private modules that should not be included in the API symbol linking""",
85+
),
86+
"import_map": attr.label_keyed_string_dict(
87+
doc = """Map of import path to the index.ts file for that import""",
88+
allow_files = True,
89+
),
90+
"module_name": attr.string(
91+
doc = """JS Module name to be used for the extracted symbols""",
92+
mandatory = True,
93+
),
94+
"module_label": attr.string(
95+
doc = """Module label to be used for the extracted symbols. To be used as display name, for example in API docs""",
96+
),
97+
"extra_entries": attr.label_list(
98+
doc = """JSON files that contain extra entries to append to the final collection.""",
99+
allow_files = True,
100+
),
101+
102+
# The executable for this rule (private).
103+
"_extract_api_to_json": attr.label(
104+
default = Label("//tools/adev-api-extraction:extract_api_to_json"),
105+
executable = True,
106+
cfg = "exec",
107+
),
108+
},
109+
)

tools/adev-api-extraction/index.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
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.dev/license
7+
*/
8+
9+
import {readFileSync, writeFileSync} from 'fs';
10+
import path from 'path';
11+
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
12+
import {
13+
ClassEntry,
14+
CompilerOptions,
15+
createCompilerHost,
16+
DocEntry,
17+
EntryCollection,
18+
InterfaceEntry,
19+
NgtscProgram,
20+
} from '@angular/compiler-cli';
21+
import ts from 'typescript';
22+
import {EXAMPLES_PATH, interpolateCodeExamples} from './interpolate_code_examples';
23+
24+
function main() {
25+
const [paramFilePath] = process.argv.slice(2);
26+
const rawParamLines = readFileSync(paramFilePath, {encoding: 'utf8'}).split('\n');
27+
28+
const [
29+
moduleName,
30+
moduleLabel,
31+
serializedPrivateModules,
32+
entryPointExecRootRelativePath,
33+
srcs,
34+
outputFilenameExecRootRelativePath,
35+
serializedPathMapWithExecRootRelativePaths,
36+
extraEntriesSrcs,
37+
] = rawParamLines;
38+
39+
const privateModules = new Set(serializedPrivateModules.split(','));
40+
41+
// The path map is a serialized JSON map of import path to index.ts file.
42+
// For example, {'@angular/core': 'path/to/some/index.ts'}
43+
const pathMap = JSON.parse(serializedPathMapWithExecRootRelativePaths) as Record<string, string>;
44+
45+
// The tsconfig expects the path map in the form of path -> array of actual locations.
46+
// We also resolve the exec root relative paths to absolute paths to disambiguate.
47+
const resolvedPathMap: {[key: string]: string[]} = {};
48+
for (const [importPath, filePath] of Object.entries(pathMap)) {
49+
resolvedPathMap[importPath] = [path.resolve(filePath)];
50+
51+
// In addition to the exact import path,
52+
// also add wildcard mappings for subdirectories.
53+
const importPathWithWildcard = path.join(importPath, '*');
54+
resolvedPathMap[importPathWithWildcard] = [
55+
path.join(path.resolve(path.dirname(filePath)), '*'),
56+
];
57+
}
58+
59+
const compilerOptions: CompilerOptions = {
60+
paths: resolvedPathMap,
61+
rootDir: '.',
62+
skipLibCheck: true,
63+
target: ts.ScriptTarget.ES2022,
64+
// This is necessary because otherwise types that include `| null` are not included in the documentation.
65+
strictNullChecks: true,
66+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
67+
experimentalDecorators: true,
68+
};
69+
70+
// Code examples should not be fed to the compiler.
71+
const filesWithoutExamples = srcs.split(',').filter(src => !src.startsWith(EXAMPLES_PATH));
72+
const compilerHost = createCompilerHost({options: compilerOptions});
73+
const program: NgtscProgram = new NgtscProgram(
74+
filesWithoutExamples,
75+
compilerOptions,
76+
compilerHost,
77+
);
78+
79+
const extraEntries: DocEntry[] = (extraEntriesSrcs ?? '')
80+
.split(',')
81+
.filter(path => !!path)
82+
.reduce((result: DocEntry[], path) => {
83+
return result.concat(JSON.parse(readFileSync(path, {encoding: 'utf8'})) as DocEntry[]);
84+
}, []);
85+
86+
const apiDoc = program.getApiDocumentation(entryPointExecRootRelativePath, privateModules);
87+
const extractedEntries = apiDoc.entries;
88+
const combinedEntries = extractedEntries.concat(extraEntries);
89+
90+
interpolateCodeExamples(combinedEntries);
91+
92+
const normalized = moduleName.replace('@', '').replace(/[\/]/g, '_');
93+
94+
const output = JSON.stringify({
95+
moduleLabel: moduleLabel || moduleName,
96+
moduleName: moduleName,
97+
normalizedModuleName: normalized,
98+
entries: combinedEntries,
99+
symbols: [
100+
// Symbols referenced, originating from other packages
101+
...apiDoc.symbols.entries(),
102+
103+
// Exported symbols from the current package
104+
...apiDoc.entries.map(entry => [entry.name, moduleName]),
105+
106+
// Also doing it for every member of classes/interfaces
107+
...apiDoc.entries.flatMap(entry => [
108+
[entry.name, moduleName],
109+
...getEntriesFromMembers(entry).map(member => [member, moduleName]),
110+
]),
111+
],
112+
} as EntryCollection);
113+
114+
writeFileSync(outputFilenameExecRootRelativePath, output, {encoding: 'utf8'});
115+
console.error('!!!!', output);
116+
}
117+
118+
function getEntriesFromMembers(entry: DocEntry): string[] {
119+
if (!hasMembers(entry)) {
120+
return [];
121+
}
122+
123+
return entry.members.map(member => `${entry.name}.${member.name}`);
124+
}
125+
126+
function hasMembers(entry: DocEntry): entry is InterfaceEntry | ClassEntry {
127+
return 'members' in entry;
128+
}
129+
130+
main();

0 commit comments

Comments
 (0)