From d2c7b4e11120ffb10e24a5e04a372079a3bf2e02 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:06:46 -0500 Subject: [PATCH] docs(docs-infra): Validate case-sensitive API symbol links in `@link` Adds build-time validation for case-sensitive API symbols in `@link`. Avoid broken links --- .../test/transforms/jsdoc-transforms.spec.mts | 17 +++++++++++++++ .../rendering/transforms/jsdoc-transforms.mts | 21 +++++++++++++++++++ packages/router/src/router_config.ts | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts index 285345badc6..fd57b323d22 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts @@ -199,6 +199,23 @@ describe('jsdoc transforms', () => { expect(entryFn).toThrowError(/Forbidden relative link: cli\/build ng build/); }); + + it('should throw on a miscased absolute @link to a known API symbol', () => { + setSymbols({RouterModule: 'router'}); + + const entryFn = () => + addHtmlAdditionalLinks({ + jsdocTags: [ + { + name: 'see', + comment: '{@link /api/router/routerModule#forRoot forRoot}', + }, + ], + moduleName: 'test', + }); + + expect(entryFn).toThrowError(/Broken @link.*Did you mean \/api\/router\/RouterModule/); + }); }); describe('addHtmlDescription', () => { diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts index 38fbe90b85d..5f067da7bb0 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts @@ -223,6 +223,27 @@ function parseAtLink(link: string): {label: string; url: string} | undefined { ); } + // Validate absolute `/api/...` links against the known symbol registry. This catches + // miscased symbol names (e.g. `/api/router/routerModule` instead of + // `/api/router/RouterModule`) at build time. + if (rawSymbol.startsWith('/api/')) { + const [pathPart] = rawSymbol.split('#'); + const segments = pathPart.split('/').filter((s) => s.length > 0); + const symbolName = segments[segments.length - 1]; + // Case-insensitive lookup: find the canonical symbol name in the registry. + const knownSymbols = Object.keys(getSymbolsAsApiEntries()); + const canonicalSymbol = knownSymbols.find( + (s) => s.toLowerCase() === symbolName.toLowerCase(), + ); + if (canonicalSymbol && canonicalSymbol !== symbolName) { + const expectedUrl = getSymbolUrl(canonicalSymbol); + throw Error( + `Broken @link: ${link}. Did you mean ${expectedUrl}? ` + + `Symbol names in API URLs are case-sensitive.`, + ); + } + } + return { url: rawSymbol, label: description ?? rawSymbol.split('/').pop()!, diff --git a/packages/router/src/router_config.ts b/packages/router/src/router_config.ts index 6926c5012f4..57dda4335be 100644 --- a/packages/router/src/router_config.ts +++ b/packages/router/src/router_config.ts @@ -214,7 +214,7 @@ export interface ComponentInputBindingOptions { * A set of configuration options for a router module, provided in the * `forRoot()` method. * - * @see {@link /api/router/routerModule#forRoot forRoot} + * @see {@link /api/router/RouterModule#forRoot forRoot} * * * @publicApi