docs(docs-infra): Add dev-mode only mention for core/global (#57365)

PR Close #57365
This commit is contained in:
Matthieu Riegler 2024-08-13 10:54:36 +02:00 committed by Andrew Kushnir
parent 07607716d4
commit 93bdbbc812
15 changed files with 137 additions and 83 deletions

View file

@ -13,6 +13,9 @@ def _extract_api_to_json(ctx):
# Pass the module_name for the extracted APIs. This will be something like "@angular/core".
args.add(ctx.attr.module_name)
# Pass the module_label for the extracted APIs, This is something like core for "@angular/core".
args.add(ctx.attr.module_label)
# Pass the entry_point for from which to extract public symbols.
args.add(ctx.file.entry_point)
@ -82,6 +85,9 @@ extract_api_to_json = rule(
doc = """JS Module name to be used for the extracted symbols""",
mandatory = True,
),
"module_label": attr.string(
doc = """Module label to be used for the extracted symbols. To be used as display name, for example in API docs""",
),
"extra_entries": attr.label_list(
doc = """JSON files that contain extra entries to append to the final collection.""",
allow_files = True,

View file

@ -1,7 +1,13 @@
import {readFileSync, writeFileSync} from 'fs';
import path from 'path';
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
import {NgtscProgram, CompilerOptions, createCompilerHost, DocEntry} from '@angular/compiler-cli';
import {
NgtscProgram,
CompilerOptions,
createCompilerHost,
DocEntry,
EntryCollection,
} from '@angular/compiler-cli';
import ts from 'typescript';
function main() {
@ -10,6 +16,7 @@ function main() {
const [
moduleName,
moduleLabel,
entryPointExecRootRelativePath,
srcs,
outputFilenameExecRootRelativePath,
@ -57,10 +64,14 @@ function main() {
const extractedEntries = program.getApiDocumentation(entryPointExecRootRelativePath);
const combinedEntries = extractedEntries.concat(extraEntries);
const normalized = moduleName.replace('@', '').replace(/[\/]/g, '_');
const output = JSON.stringify({
moduleLabel: moduleLabel || moduleName,
moduleName: moduleName,
normalizedModuleName: normalized,
entries: combinedEntries,
});
} satisfies EntryCollection);
writeFileSync(outputFilenameExecRootRelativePath, output, {encoding: 'utf8'});
}

View file

@ -1,13 +1,14 @@
load("//adev/shared-docs/pipeline/api-gen/extraction:extract_api_to_json.bzl", "extract_api_to_json")
load("//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html.bzl", "render_api_to_html")
def generate_api_docs(name, module_name, entry_point, srcs, import_map = {}, extra_entries = []):
def generate_api_docs(name, module_name, entry_point, srcs, module_label = None, import_map = {}, extra_entries = []):
"""Generates API documentation reference pages for the given sources."""
json_outfile = name + "_api.json"
extract_api_to_json(
name = name + "_extraction",
module_name = module_name,
module_label = module_label,
entry_point = entry_point,
srcs = srcs,
output_name = json_outfile,

View file

@ -1,11 +1,5 @@
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
import type {DocEntry, JsDocTagEntry} from '@angular/compiler-cli';
/** The JSON data file format for extracted API reference info. */
export interface EntryCollection {
moduleName: string;
entries: DocEntry[];
}
import type {DocEntry, EntryCollection, JsDocTagEntry} from '@angular/compiler-cli';
export interface ManifestEntry {
name: string;
@ -16,7 +10,12 @@ export interface ManifestEntry {
}
/** Manifest that maps each module name to a list of API symbols. */
export type Manifest = Record<string, ManifestEntry[]>;
export type Manifest = {
moduleName: string;
normalizedModuleName: string;
moduleLabel: string;
entries: ManifestEntry[];
}[];
/** Gets a unique lookup key for an API, e.g. "@angular/core/ElementRef". */
function getApiLookupKey(moduleName: string, name: string) {
@ -114,22 +113,39 @@ export function generateManifest(apiCollections: EntryCollection[]): Manifest {
});
}
const manifest: Manifest = {};
const manifest: Manifest = [];
for (const collection of apiCollections) {
if (!manifest[collection.moduleName]) {
manifest[collection.moduleName] = [];
const entries = collection.entries.map((entry) => ({
name: entry.name,
type: entry.entryType,
isDeprecated: isDeprecated(entryLookup, collection.moduleName, entry),
isDeveloperPreview: isDeveloperPreview(entryLookup, collection.moduleName, entry),
isExperimental: isExperimental(entryLookup, collection.moduleName, entry),
}));
const existingEntry = manifest.find((entry) => entry.moduleName === collection.moduleName);
if (existingEntry) {
existingEntry.entries.push(...entries);
} else {
manifest.push({
moduleName: collection.moduleName,
normalizedModuleName: collection.normalizedModuleName,
moduleLabel: collection.moduleLabel ?? collection.moduleName,
entries,
});
}
}
manifest.sort((entry1, entry2) => {
// Ensure that labels that start with a `code` tag like `window.ng` are last
if (entry1.moduleLabel.startsWith('<')) {
return 1;
} else if (entry2.moduleLabel.startsWith('<')) {
return -1;
}
manifest[collection.moduleName].push(
...collection.entries.map((entry) => ({
name: entry.name,
type: entry.entryType,
isDeprecated: isDeprecated(entryLookup, collection.moduleName, entry),
isDeveloperPreview: isDeveloperPreview(entryLookup, collection.moduleName, entry),
isExperimental: isExperimental(entryLookup, collection.moduleName, entry),
})),
);
}
return entry1.moduleLabel.localeCompare(entry2.moduleLabel);
});
return manifest;
}

View file

@ -1,5 +1,6 @@
import {readFileSync, writeFileSync} from 'fs';
import {EntryCollection, generateManifest} from './generate_manifest';
import {generateManifest} from './generate_manifest';
import type {EntryCollection} from '@angular/compiler-cli';
function main() {
const [paramFilePath] = process.argv.slice(2);

View file

@ -11,6 +11,8 @@ import {initHighlighter} from './shiki/shiki';
/** The JSON data file format for extracted API reference info. */
interface EntryCollection {
moduleName: string;
moduleLabel?: string;
normalizedModuleName: string;
entries: DocEntry[];
}
@ -30,11 +32,13 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
return [
{
moduleName: 'unknown',
normalizedModuleName: 'unknown',
entries: [fileContentJson as DocEntry],
},
...command.subcommands!.map((subCommand) => {
return {
moduleName: 'unknown',
normalizedModuleName: 'unknown',
entries: [{...subCommand, parentCommand: command} as any],
};
}),
@ -43,13 +47,14 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
return {
moduleName: 'unknown',
normalizedModuleName: 'unknown',
entries: [fileContentJson as DocEntry], // TODO: fix the typing cli entries aren't DocEntry
};
});
}
/** Gets a normalized filename for a doc entry. */
function getNormalizedFilename(moduleName: string, entry: DocEntry | CliCommand): string {
function getNormalizedFilename(normalizedModuleName: string, entry: DocEntry | CliCommand): string {
if (isCliEntry(entry)) {
return entry.parentCommand
? `${entry.parentCommand.name}/${entry.name}.html`
@ -57,9 +62,6 @@ function getNormalizedFilename(moduleName: string, entry: DocEntry | CliCommand)
}
entry = entry as DocEntry;
// Angular entry points all contain an "@" character, which we want to remove
// from the filename. We also swap `/` with an underscore.
const normalizedModuleName = moduleName.replace('@', '').replace(/\//g, '_');
// Append entry type as suffix to prevent writing to file that only differs in casing or query string from already written file.
// This will lead to a race-condition and corrupted files on case-insensitive file systems.
@ -103,7 +105,10 @@ async function main() {
const htmlOutputs = renderableEntries.map(renderEntry);
for (let i = 0; i < htmlOutputs.length; i++) {
const filename = getNormalizedFilename(collection.moduleName, collection.entries[i]);
const filename = getNormalizedFilename(
collection.normalizedModuleName,
collection.entries[i],
);
const outputPath = path.join(outputFilenameExecRootRelativePath, filename);
// in case the output path is nested, ensure the directory exists

View file

@ -10,8 +10,8 @@ import {HttpClient} from '@angular/common/http';
import {Injectable, inject} from '@angular/core';
import {DocContent, DocsContentLoader} from '@angular/docs';
import {Router} from '@angular/router';
import {firstValueFrom} from 'rxjs';
import {map} from 'rxjs/operators';
import {firstValueFrom, of} from 'rxjs';
import {catchError, map} from 'rxjs/operators';
@Injectable()
export class ContentLoader implements DocsContentLoader {

View file

@ -3,14 +3,26 @@
@if (group.isFeatured) {
<docs-icon aria-hidden>star</docs-icon>
}
<a routerLink="/api" [fragment]="group.id" queryParamsHandling="preserve" class="adev-api-anchor" tabindex="-1">{{ group.title }}</a>
<!-- we use innerHtml because the title can be an html string-->
<a
routerLink="/api"
[fragment]="group.id"
queryParamsHandling="preserve"
class="adev-api-anchor"
tabindex="-1"
[innerHtml]="group.title"
></a>
</h3>
</header>
<ul class="adev-api-items-section-grid">
@for (apiItem of group.items; track apiItem.url) {
<li [class.adev-api-items-section-item-deprecated]="apiItem.isDeprecated">
<a [routerLink]="'/' + apiItem.url" class="adev-api-items-section-item" [attr.aria-describedby]="apiItem.isDeprecated ? 'deprecated-description' : null">
<a
[routerLink]="'/' + apiItem.url"
class="adev-api-items-section-item"
[attr.aria-describedby]="apiItem.isDeprecated ? 'deprecated-description' : null"
>
<docs-api-item-label
[type]="apiItem.itemType"
mode="short"
@ -20,9 +32,7 @@
<span class="adev-item-title">{{ apiItem.title }}</span>
</a>
@if (apiItem.isDeprecated) {
<span class="docs-deprecated">
&lt;!&gt;
</span>
<span class="docs-deprecated"> &lt;!&gt; </span>
}
@if (apiItem.isFeatured) {
<docs-icon

View file

@ -9,7 +9,7 @@
import {Injectable, signal} from '@angular/core';
// This file is generated at build-time, error is expected here.
import API_MANIFEST_JSON from '../../../../../src/assets/api/manifest.json';
import {ANGULAR_PACKAGE_PREFIX, getApiUrl} from '../helpers/manifest.helper';
import {getApiUrl} from '../helpers/manifest.helper';
import {ApiItem} from '../interfaces/api-item';
import {ApiItemsGroup} from '../interfaces/api-items-group';
import {ApiManifest} from '../interfaces/api-manifest';
@ -34,6 +34,8 @@ export const FEATURED_ITEMS_URLS = [
'api/router/CanActivate',
];
const manifest = API_MANIFEST_JSON as ApiManifest;
@Injectable({
providedIn: 'root',
})
@ -50,20 +52,14 @@ export class ApiReferenceManager {
private mapManifestToApiGroups(): ApiItemsGroup[] {
const groups: ApiItemsGroup[] = [];
const manifest = API_MANIFEST_JSON as ApiManifest;
const packageNames = Object.keys(API_MANIFEST_JSON);
for (const packageName of packageNames) {
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
const packageApis = manifest[packageName];
for (const module of manifest) {
groups.push({
title: packageNameWithoutPrefix,
id: packageNameWithoutPrefix.replace(/\//g, '-'),
items: packageApis
title: module.moduleLabel.replace('@angular/', ''),
id: module.normalizedModuleName,
items: module.entries
.map((api) => {
const url = getApiUrl(packageNameWithoutPrefix, api.name);
const url = getApiUrl(module, api.name);
const isFeatured = FEATURED_ITEMS_URLS.some((featuredUrl) => featuredUrl === url);
const apiItem = {
itemType: api.type,

View file

@ -8,31 +8,23 @@
import {Route} from '@angular/router';
import API_MANIFEST_JSON from '../../../../../src/assets/api/manifest.json';
import {ApiManifest, ApiManifestItem} from '../interfaces/api-manifest';
import {ApiManifest, ApiManifestEntry, ApiManifestPackage} from '../interfaces/api-manifest';
import {PagePrefix} from '../../../core/enums/pages';
import {NavigationItem, contentResolver} from '@angular/docs';
export const ANGULAR_PACKAGE_PREFIX = '@angular/';
const manifest = API_MANIFEST_JSON as ApiManifest;
export function mapApiManifestToRoutes(): Route[] {
const manifest = API_MANIFEST_JSON as ApiManifest;
const packageNames = Object.keys(API_MANIFEST_JSON);
const apiRoutes: Route[] = [];
for (const packageName of packageNames) {
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
const packageApis = manifest[packageName];
for (const api of packageApis) {
for (const packageEntry of manifest) {
for (const api of packageEntry.entries) {
apiRoutes.push({
path: getApiUrl(packageNameWithoutPrefix, api.name),
path: getApiUrl(packageEntry, api.name),
loadComponent: () =>
import('./../api-reference-details-page/api-reference-details-page.component'),
resolve: {
docContent: contentResolver(
`api/${getNormalizedFilename(packageNameWithoutPrefix, api)}`,
),
docContent: contentResolver(`api/${getNormalizedFilename(packageEntry, api)}`),
},
data: {
label: api.name,
@ -46,20 +38,14 @@ export function mapApiManifestToRoutes(): Route[] {
}
export function getApiNavigationItems(): NavigationItem[] {
const manifest = API_MANIFEST_JSON as ApiManifest;
const packageNames = Object.keys(API_MANIFEST_JSON);
const apiNavigationItems: NavigationItem[] = [];
for (const packageName of packageNames) {
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
const packageApis = manifest[packageName];
for (const packageEntry of manifest) {
const packageNavigationItem: NavigationItem = {
label: packageNameWithoutPrefix,
children: packageApis
label: packageEntry.moduleLabel,
children: packageEntry.entries
.map((api) => ({
path: getApiUrl(packageNameWithoutPrefix, api.name),
path: getApiUrl(packageEntry, api.name),
label: api.name,
}))
.sort((a, b) => a.label.localeCompare(b.label)),
@ -71,12 +57,13 @@ export function getApiNavigationItems(): NavigationItem[] {
return apiNavigationItems;
}
export function getApiUrl(packageNameWithoutPrefix: string, apiName: string): string {
return `${PagePrefix.API}/${packageNameWithoutPrefix}/${apiName}`;
export function getApiUrl(packageEntry: ApiManifestPackage, apiName: string): string {
return `${PagePrefix.API}/${packageEntry.normalizedModuleName}/${apiName}`;
}
function getNormalizedFilename(moduleName: string, entry: ApiManifestItem): string {
// Angular entry points can contain `/`, we would like to swap `/` with an underscore
const normalizedModuleName = moduleName.replace(/\//g, '_');
return `angular_${normalizedModuleName}_${entry.name}_${entry.type}.html`;
function getNormalizedFilename(
manifestPackage: ApiManifestPackage,
entry: ApiManifestEntry,
): string {
return `${manifestPackage.normalizedModuleName}_${entry.name}_${entry.type}.html`;
}

View file

@ -8,12 +8,17 @@
import {ApiItemType} from './api-item-type';
export interface ApiManifestItem {
export interface ApiManifestEntry {
name: string;
type: ApiItemType;
isDeprecated?: boolean;
}
export interface ApiManifest {
[packageName: string]: ApiManifestItem[];
export interface ApiManifestPackage {
moduleName: string;
normalizedModuleName: string;
moduleLabel: string;
entries: ApiManifestEntry[];
}
export type ApiManifest = ApiManifestPackage[];

View file

@ -6,6 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/
/** The JSON data file format for extracted API reference info. */
export interface EntryCollection {
moduleName: string;
// The normalized name is shared so rendering and manifest use the same common field
normalizedModuleName: string;
moduleLabel: string;
entries: DocEntry[];
}
/** Type of top-level documentation entry. */
export enum EntryType {
Block = 'block',

View file

@ -16,5 +16,6 @@ generate_api_docs(
"//packages:common_files_and_deps_for_docs",
],
entry_point = ":index.ts",
module_name = "@angular/core/global",
module_label = "<code>window.ng</code> globals",
module_name = "@angular/core/globals",
)

View file

@ -41,6 +41,8 @@ function main() {
outputFileExecRootRelativePath,
JSON.stringify({
moduleName: '@angular/core',
normalizedModuleName: 'angular_core',
moduleLabel: 'core',
entries,
}),
{encoding: 'utf8'},

View file

@ -36,6 +36,8 @@ function main() {
outputFileExecRootRelativePath,
JSON.stringify({
moduleName: '@angular/core',
normalizedModuleName: 'angular_core',
moduleLabel: 'core',
entries,
}),
{encoding: 'utf8'},