mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs(docs-infra): Add dev-mode only mention for core/global (#57365)
PR Close #57365
This commit is contained in:
parent
07607716d4
commit
93bdbbc812
15 changed files with 137 additions and 83 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<!>
|
||||
</span>
|
||||
<span class="docs-deprecated"> <!> </span>
|
||||
}
|
||||
@if (apiItem.isFeatured) {
|
||||
<docs-icon
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ function main() {
|
|||
outputFileExecRootRelativePath,
|
||||
JSON.stringify({
|
||||
moduleName: '@angular/core',
|
||||
normalizedModuleName: 'angular_core',
|
||||
moduleLabel: 'core',
|
||||
entries,
|
||||
}),
|
||||
{encoding: 'utf8'},
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ function main() {
|
|||
outputFileExecRootRelativePath,
|
||||
JSON.stringify({
|
||||
moduleName: '@angular/core',
|
||||
normalizedModuleName: 'angular_core',
|
||||
moduleLabel: 'core',
|
||||
entries,
|
||||
}),
|
||||
{encoding: 'utf8'},
|
||||
|
|
|
|||
Loading…
Reference in a new issue