diff --git a/adev/shared-docs/pipeline/api-gen/extraction/extract_api_to_json.bzl b/adev/shared-docs/pipeline/api-gen/extraction/extract_api_to_json.bzl index bd94eda9993..461c50669f6 100644 --- a/adev/shared-docs/pipeline/api-gen/extraction/extract_api_to_json.bzl +++ b/adev/shared-docs/pipeline/api-gen/extraction/extract_api_to_json.bzl @@ -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, diff --git a/adev/shared-docs/pipeline/api-gen/extraction/index.ts b/adev/shared-docs/pipeline/api-gen/extraction/index.ts index 8d07b7c6abe..6b6d89062ec 100644 --- a/adev/shared-docs/pipeline/api-gen/extraction/index.ts +++ b/adev/shared-docs/pipeline/api-gen/extraction/index.ts @@ -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'}); } diff --git a/adev/shared-docs/pipeline/api-gen/generate_api_docs.bzl b/adev/shared-docs/pipeline/api-gen/generate_api_docs.bzl index 21ddb39a7b6..879e7191c64 100644 --- a/adev/shared-docs/pipeline/api-gen/generate_api_docs.bzl +++ b/adev/shared-docs/pipeline/api-gen/generate_api_docs.bzl @@ -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, diff --git a/adev/shared-docs/pipeline/api-gen/manifest/generate_manifest.ts b/adev/shared-docs/pipeline/api-gen/manifest/generate_manifest.ts index 7912adb90b9..ce46e92e5fe 100644 --- a/adev/shared-docs/pipeline/api-gen/manifest/generate_manifest.ts +++ b/adev/shared-docs/pipeline/api-gen/manifest/generate_manifest.ts @@ -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; +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; } diff --git a/adev/shared-docs/pipeline/api-gen/manifest/index.ts b/adev/shared-docs/pipeline/api-gen/manifest/index.ts index faf5398f9b2..39cbc8f244b 100644 --- a/adev/shared-docs/pipeline/api-gen/manifest/index.ts +++ b/adev/shared-docs/pipeline/api-gen/manifest/index.ts @@ -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); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/index.ts b/adev/shared-docs/pipeline/api-gen/rendering/index.ts index 7301455d53e..a4ed03aa08f 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/index.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/index.ts @@ -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 diff --git a/adev/src/app/core/services/content-loader.service.ts b/adev/src/app/core/services/content-loader.service.ts index f4579acb5f1..01ba2e8a354 100644 --- a/adev/src/app/core/services/content-loader.service.ts +++ b/adev/src/app/core/services/content-loader.service.ts @@ -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 { diff --git a/adev/src/app/features/references/api-items-section/api-items-section.component.html b/adev/src/app/features/references/api-items-section/api-items-section.component.html index 0e951948b5d..459f343ee61 100644 --- a/adev/src/app/features/references/api-items-section/api-items-section.component.html +++ b/adev/src/app/features/references/api-items-section/api-items-section.component.html @@ -3,14 +3,26 @@ @if (group.isFeatured) { star } - {{ group.title }} + +